mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-27 12:04:56 +02:00
Merge branch 'staging' into develop
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
"0.6.431"
|
||||
"0.6.435"
|
||||
|
||||
@@ -401,6 +401,10 @@ export const documentOperation: MigrateOperation = {
|
||||
{
|
||||
state: 'accounts-to-social-ids',
|
||||
func: migrateAccountsToSocialIds
|
||||
},
|
||||
{
|
||||
state: 'migrateEmbeddingsRefs',
|
||||
func: migrateEmbeddingsRefs
|
||||
}
|
||||
])
|
||||
},
|
||||
@@ -416,3 +420,13 @@ async function migrateEmbeddings (client: MigrationClient): Promise<void> {
|
||||
)
|
||||
await client.move(DOMAIN_DOCUMENT, { _class: attachment.class.Embedding }, DOMAIN_ATTACHMENT)
|
||||
}
|
||||
|
||||
async function migrateEmbeddingsRefs (client: MigrationClient): Promise<void> {
|
||||
const _class = 'document:class:DocumentEmbedding'
|
||||
|
||||
await client.update(DOMAIN_ACTIVITY, { attachedToClass: _class }, { attachedToClass: attachment.class.Embedding })
|
||||
await client.update(DOMAIN_ACTIVITY, { objectClass: _class }, { objectClass: attachment.class.Embedding })
|
||||
await client.update(DOMAIN_NOTIFICATION, { attachedToClass: _class }, { attachedToClass: attachment.class.Embedding })
|
||||
await client.update(DOMAIN_TX, { objectClass: _class }, { objectClass: attachment.class.Embedding })
|
||||
await client.update(DOMAIN_TX, { 'tx.objectClass': _class }, { 'tx.objectClass': attachment.class.Embedding })
|
||||
}
|
||||
|
||||
@@ -12,17 +12,9 @@
|
||||
"esModuleInterop": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["esnext", "dom"],
|
||||
"incremental": true,
|
||||
"types": [
|
||||
"jest"
|
||||
],
|
||||
"types": ["jest"],
|
||||
"isolatedModules": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
if (res.length > 0) {
|
||||
return res
|
||||
}
|
||||
clazz = hierarchy.getClass(_class).extends
|
||||
clazz = hierarchy.getClass(clazz).extends
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"Major": "Мажорная",
|
||||
"Minor": "Минорная",
|
||||
"Patch": "Патч",
|
||||
"DocumentApprovals": "Рецензии и утверждения",
|
||||
"ValidationWorkflow": "Процесс валидации",
|
||||
"ChangeOwner": "Изменить владельца документа",
|
||||
"ChangeOwnerHintBeginning": "Передайте права владельца документа",
|
||||
"ChangeOwnerHintEnd": "другому лицу.",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"Major": "主要",
|
||||
"Minor": "次要",
|
||||
"Patch": "补丁",
|
||||
"DocumentApprovals": "审查和批准",
|
||||
"ValidationWorkflow": "验证工作流程",
|
||||
"ChangeOwner": "更改文档所有者",
|
||||
"ChangeOwnerHintBeginning": "转移所有权",
|
||||
"ChangeOwnerHintEnd": "给其他人。",
|
||||
|
||||
@@ -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')
|
||||
|
||||
+50
-103
@@ -13,121 +13,68 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import documents, { DocumentApprovalRequest, DocumentReviewRequest } from '@hcengineering/controlled-documents'
|
||||
import { employeeByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Employee, Person, formatName } from '@hcengineering/contact'
|
||||
import { employeeByIdStore, personIdByAccountId } from '@hcengineering/contact-resources'
|
||||
import documents, {
|
||||
DocumentRequest,
|
||||
emptyBundle,
|
||||
extractValidationWorkflow
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
|
||||
import documentsRes from '../../plugin'
|
||||
import { $controlledDocument as controlledDocument } from '../../stores/editors/document/editor'
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
$documentSnapshots as documentSnapshots
|
||||
} from '../../stores/editors/document/editor'
|
||||
import { formatSignatureDate } from '../../utils'
|
||||
|
||||
interface Signer {
|
||||
id?: Ref<Person>
|
||||
role: 'author' | 'reviewer' | 'approver'
|
||||
name: string
|
||||
date: string
|
||||
let requests: DocumentRequest[] = []
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: doc = $controlledDocument
|
||||
|
||||
$: if (doc) {
|
||||
void client.findAll(documents.class.DocumentRequest, { attachedTo: doc._id }).then((r) => {
|
||||
requests = r
|
||||
})
|
||||
}
|
||||
|
||||
let signers: Signer[] = []
|
||||
$: workflow = extractValidationWorkflow(
|
||||
hierarchy,
|
||||
{
|
||||
...emptyBundle(),
|
||||
ControlledDocument: doc ? [doc] : [],
|
||||
DocumentRequest: requests,
|
||||
DocumentSnapshot: $documentSnapshots
|
||||
},
|
||||
(ref) => $personIdByAccountId.get(ref)
|
||||
)
|
||||
|
||||
let reviewRequest: DocumentReviewRequest
|
||||
let approvalRequest: DocumentApprovalRequest
|
||||
|
||||
const reviewQuery = createQuery()
|
||||
const approvalQuery = createQuery()
|
||||
|
||||
$: if ($controlledDocument !== undefined) {
|
||||
reviewQuery.query(
|
||||
documents.class.DocumentReviewRequest,
|
||||
{
|
||||
attachedTo: $controlledDocument?._id,
|
||||
attachedToClass: $controlledDocument?._class
|
||||
},
|
||||
(res) => {
|
||||
reviewRequest = res[0]
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
limit: 1
|
||||
$: state = (doc ? workflow?.get(doc._id) ?? [] : [])[0]
|
||||
$: signers = (state?.approvals ?? [])
|
||||
.filter((a) => a.state === 'approved')
|
||||
.map((a) => {
|
||||
return {
|
||||
person: a.person,
|
||||
role: a.role,
|
||||
name: getNameByEmployeeId(a.person),
|
||||
date: a.timestamp ? formatSignatureDate(a.timestamp) : ''
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
approvalQuery.query(
|
||||
documents.class.DocumentApprovalRequest,
|
||||
{
|
||||
attachedTo: $controlledDocument?._id,
|
||||
attachedToClass: $controlledDocument?._class
|
||||
},
|
||||
(res) => {
|
||||
approvalRequest = res[0]
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
} else {
|
||||
reviewQuery.unsubscribe()
|
||||
approvalQuery.unsubscribe()
|
||||
}
|
||||
function getNameByEmployeeId (id: Ref<Person> | undefined): string {
|
||||
if (id === undefined) return ''
|
||||
|
||||
$: if ($controlledDocument !== null) {
|
||||
const getNameByEmployeeId = (id: Ref<Person> | undefined): string => {
|
||||
if (id === undefined) {
|
||||
return ''
|
||||
}
|
||||
const employee = $employeeByIdStore.get(id as Ref<Employee>)
|
||||
const rawName = employee?.name
|
||||
|
||||
const employee = $employeeByIdStore.get(id as Ref<Employee>)
|
||||
const rawName = employee?.name
|
||||
|
||||
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: authorSignDate !== undefined ? formatSignatureDate(authorSignDate) : ''
|
||||
}
|
||||
]
|
||||
|
||||
if (reviewRequest !== undefined) {
|
||||
reviewRequest.approved.forEach((reviewer, idx) => {
|
||||
const date = reviewRequest.approvedDates?.[idx]
|
||||
|
||||
signers.push({
|
||||
id: reviewer,
|
||||
role: 'reviewer',
|
||||
name: getNameByEmployeeId(reviewer),
|
||||
date: formatSignatureDate(date ?? reviewRequest.modifiedOn)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (approvalRequest !== undefined) {
|
||||
approvalRequest.approved.forEach((approver, idx) => {
|
||||
const date = approvalRequest.approvedDates?.[idx]
|
||||
|
||||
signers.push({
|
||||
id: approver,
|
||||
role: 'approver',
|
||||
name: getNameByEmployeeId(approver),
|
||||
date: formatSignatureDate(date ?? approvalRequest.modifiedOn)
|
||||
})
|
||||
})
|
||||
}
|
||||
return rawName !== undefined ? formatName(rawName) : ''
|
||||
}
|
||||
|
||||
function getSignerLabel (role: 'author' | 'reviewer' | 'approver'): IntlString {
|
||||
@@ -161,7 +108,7 @@
|
||||
{signer.name}
|
||||
</div>
|
||||
<div class="code">
|
||||
{signer.id}
|
||||
{signer.person}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+31
-90
@@ -1,93 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import documents, { DocumentRequest } from '@hcengineering/controlled-documents'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { type Person } from '@hcengineering/contact'
|
||||
import { PersonRefPresenter } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { DocumentValidationState } from '@hcengineering/controlled-documents'
|
||||
import { Chevron, Label, tooltip } from '@hcengineering/ui'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
import { $documentSnapshots as documentSnapshots } from '../../../stores/editors/document'
|
||||
import documentsRes from '../../../plugin'
|
||||
import ApprovedIcon from '../../icons/Approved.svelte'
|
||||
import RejectedIcon from '../../icons/Rejected.svelte'
|
||||
import CancelledIcon from '../../icons/Cancelled.svelte'
|
||||
import RejectedIcon from '../../icons/Rejected.svelte'
|
||||
import WaitingIcon from '../../icons/Waiting.svelte'
|
||||
import SignatureInfo from './SignatureInfo.svelte'
|
||||
|
||||
export let request: DocumentRequest
|
||||
export let state: DocumentValidationState
|
||||
export let initiallyExpanded: boolean = false
|
||||
|
||||
interface PersonalApproval {
|
||||
person?: Ref<Person>
|
||||
approved: 'approved' | 'rejected' | 'cancelled' | 'waiting'
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let expanded: boolean = initiallyExpanded
|
||||
|
||||
let rejectingMessage: string | undefined
|
||||
let approvals: PersonalApproval[] = []
|
||||
|
||||
$: void getRequestData(request)
|
||||
|
||||
$: type = hierarchy.isDerived(request._class, documents.class.DocumentApprovalRequest)
|
||||
? documents.string.Approval
|
||||
: documents.string.Review
|
||||
|
||||
async function getRequestData (req: DocumentRequest): Promise<void> {
|
||||
if (req == null) {
|
||||
return
|
||||
}
|
||||
|
||||
approvals = await getApprovals(req)
|
||||
const rejectingComment = await client.findOne(chunter.class.ChatMessage, {
|
||||
attachedTo: req?._id,
|
||||
attachedToClass: req?._class
|
||||
})
|
||||
rejectingMessage = rejectingComment?.message
|
||||
}
|
||||
|
||||
async function getApprovals (req: DocumentRequest): Promise<PersonalApproval[]> {
|
||||
const rejectedBy: PersonalApproval[] =
|
||||
req.rejected !== undefined
|
||||
? [
|
||||
{
|
||||
person: req.rejected,
|
||||
approved: 'rejected',
|
||||
timestamp: req.modifiedOn
|
||||
}
|
||||
]
|
||||
: []
|
||||
const approvedBy: PersonalApproval[] = req.approved.map((id, idx) => ({
|
||||
person: id,
|
||||
approved: 'approved',
|
||||
timestamp: req.approvedDates?.[idx] ?? req.modifiedOn
|
||||
}))
|
||||
const ignoredBy = req.requested
|
||||
.filter((p) => p !== req?.rejected)
|
||||
.filter((p) => !(req?.approved as string[]).includes(p))
|
||||
.map(
|
||||
(id): PersonalApproval => ({
|
||||
person: id,
|
||||
approved: req?.rejected !== undefined ? 'cancelled' : 'waiting'
|
||||
})
|
||||
)
|
||||
return [...approvedBy, ...rejectedBy, ...ignoredBy]
|
||||
}
|
||||
|
||||
$: snapshot = $documentSnapshots
|
||||
.toReversed()
|
||||
.find((s) => s.createdOn !== undefined && request.createdOn !== undefined && s.createdOn > request.createdOn)
|
||||
|
||||
const dtf = new Intl.DateTimeFormat('default', {
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
})
|
||||
|
||||
$: snapshot = state?.snapshot
|
||||
$: approvals = state?.approvals ?? []
|
||||
|
||||
const roleString = {
|
||||
author: documentsRes.string.Author,
|
||||
reviewer: documentsRes.string.Reviewer,
|
||||
approver: documentsRes.string.Approver
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -106,9 +47,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span><Label label={type} /></span>
|
||||
<span>•</span>
|
||||
<span class="date">{dtf.format(request?.modifiedOn)}</span>
|
||||
<span class="date">{dtf.format(state?.modifiedOn)}</span>
|
||||
<div class="chevron" class:visible={expanded}>
|
||||
<Chevron outline {expanded} size={'small'} />
|
||||
</div>
|
||||
@@ -116,37 +55,39 @@
|
||||
</button>
|
||||
{#if expanded}
|
||||
<div class="section" transition:slide|local>
|
||||
{#each approvals as approver}
|
||||
{#each approvals as approval}
|
||||
{@const messages = approval.messages ?? []}
|
||||
<div class="approver">
|
||||
<PersonRefPresenter value={approver.person} avatarSize="x-small" />
|
||||
{#key approver.timestamp}
|
||||
<!-- For some reason tooltip is not interactive w/o remount -->
|
||||
<PersonRefPresenter value={approval.person} avatarSize="x-small" />
|
||||
{#key approval.timestamp}
|
||||
<span
|
||||
use:tooltip={approver.timestamp !== undefined
|
||||
class="flex gap-1"
|
||||
use:tooltip={approval.timestamp !== undefined
|
||||
? {
|
||||
component: SignatureInfo,
|
||||
props: {
|
||||
id: approver.person,
|
||||
timestamp: approver.timestamp
|
||||
id: approval.person,
|
||||
timestamp: approval.timestamp
|
||||
}
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
{#if approver.approved === 'approved'}
|
||||
<span><Label label={roleString[approval.role]} /></span>
|
||||
{#if approval.state === 'approved'}
|
||||
<ApprovedIcon size="medium" fill={'var(--theme-docs-accepted-color)'} />
|
||||
{:else if approver.approved === 'rejected'}
|
||||
{:else if approval.state === 'rejected'}
|
||||
<RejectedIcon size="medium" fill={'var(--negative-button-default)'} />
|
||||
{:else if approver.approved === 'cancelled'}
|
||||
{:else if approval.state === 'cancelled'}
|
||||
<CancelledIcon size="medium" />
|
||||
{:else if approver.approved === 'waiting'}
|
||||
{:else if approval.state === 'waiting'}
|
||||
<WaitingIcon size="medium" />
|
||||
{/if}
|
||||
</span>
|
||||
{/key}
|
||||
</div>
|
||||
{#if rejectingMessage !== undefined && approver.approved === 'rejected'}
|
||||
<div class="reject-message">{rejectingMessage}</div>
|
||||
{/if}
|
||||
{#each messages as m}
|
||||
<div class="approval-status-message">{m.message}</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -196,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;
|
||||
}
|
||||
|
||||
+49
-34
@@ -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)
|
||||
</script>
|
||||
|
||||
<RightPanelTabHeader>
|
||||
<Label label={document.string.DocumentApprovals} />
|
||||
<Label label={documentsRes.string.ValidationWorkflow} />
|
||||
</RightPanelTabHeader>
|
||||
|
||||
<Scroller>
|
||||
@@ -60,13 +75,13 @@
|
||||
<DocumentApprovalGuideItem />
|
||||
</div>
|
||||
{/if}
|
||||
{#if requests.length > 0}
|
||||
{#each requests as object, idx}
|
||||
<DocumentApprovalItem request={object} initiallyExpanded={!hasGuide && idx === 0} />
|
||||
{#if validationStates.length > 0}
|
||||
{#each validationStates as state, idx}
|
||||
<DocumentApprovalItem {state} initiallyExpanded={!hasGuide && idx === 0} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if !hasGuide && approvals.length === 0}
|
||||
<div class="no-approvals-message"><Label label={document.string.NoApprovalsDescription} /></div>
|
||||
{#if !hasGuide && requests.length === 0}
|
||||
<div class="no-approvals-message"><Label label={documentsRes.string.NoApprovalsDescription} /></div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
|
||||
+36
-146
@@ -14,183 +14,70 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { type Ref, SortingOrder, notEmpty } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import documents, {
|
||||
type ControlledDocument,
|
||||
type DocumentMeta,
|
||||
import {
|
||||
DocumentBundle,
|
||||
ProjectDocumentTree,
|
||||
type HierarchyDocument,
|
||||
type Project,
|
||||
type ProjectMeta
|
||||
type Project
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
|
||||
import { compareDocs } from '../../../../utils'
|
||||
import { createDocumentHierarchyQuery } from '../../../../utils'
|
||||
import DocumentFlatTreeElement from './DocumentFlatTreeElement.svelte'
|
||||
|
||||
export let document: HierarchyDocument | undefined
|
||||
export let project: Ref<Project>
|
||||
|
||||
const currentPerson = getCurrentEmployee()
|
||||
let tree = new ProjectDocumentTree()
|
||||
|
||||
let meta: ProjectMeta | undefined
|
||||
const projectMetaQuery = createQuery()
|
||||
const query = createDocumentHierarchyQuery()
|
||||
$: if (document !== undefined && project !== undefined) {
|
||||
projectMetaQuery.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
project,
|
||||
meta: document.attachedTo
|
||||
},
|
||||
(result) => {
|
||||
;[meta] = result
|
||||
}
|
||||
)
|
||||
query.query(document.space, project, (data) => {
|
||||
tree = data
|
||||
})
|
||||
}
|
||||
|
||||
let parentMetas: ProjectMeta[] | undefined
|
||||
const parentMetasQuery = createQuery()
|
||||
$: if (meta !== undefined) {
|
||||
parentMetasQuery.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
meta: { $in: meta.path },
|
||||
project: meta.project
|
||||
},
|
||||
(result) => {
|
||||
parentMetas = result
|
||||
}
|
||||
)
|
||||
} else {
|
||||
parentMetasQuery.unsubscribe()
|
||||
}
|
||||
$: docmetaid = tree.metaOf(document?._id)
|
||||
|
||||
let directChildrenMetas: ProjectMeta[] | undefined
|
||||
const childrenMetasQuery = createQuery()
|
||||
$: if (meta !== undefined) {
|
||||
childrenMetasQuery.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
parent: meta?.meta,
|
||||
project: meta.project
|
||||
},
|
||||
(result) => {
|
||||
if (meta === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
directChildrenMetas = result
|
||||
}
|
||||
)
|
||||
} else {
|
||||
childrenMetasQuery.unsubscribe()
|
||||
}
|
||||
|
||||
let docs: Record<Ref<DocumentMeta>, ControlledDocument> = {}
|
||||
const docsQuery = createQuery()
|
||||
$: if (parentMetas !== undefined && directChildrenMetas !== undefined) {
|
||||
docsQuery.query(
|
||||
documents.class.ProjectDocument,
|
||||
{
|
||||
attachedTo: { $in: [...parentMetas.map((p) => p._id), ...directChildrenMetas.map((p) => p._id)] }
|
||||
},
|
||||
(result) => {
|
||||
docs = {}
|
||||
let lastTemplate: string | undefined = '###'
|
||||
let lastSeqNumber = -1
|
||||
|
||||
for (const prjdoc of result) {
|
||||
const doc = prjdoc.$lookup?.document as ControlledDocument | undefined
|
||||
if (doc === undefined) continue
|
||||
|
||||
// TODO add proper fix, when document with no template copied, saved value is null
|
||||
const template = doc.template ?? undefined
|
||||
|
||||
if (template === lastTemplate && doc.seqNumber === lastSeqNumber) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
doc.owner === currentPerson ||
|
||||
doc.coAuthors.findIndex((emp) => emp === currentPerson) >= 0 ||
|
||||
doc.approvers.findIndex((emp) => emp === currentPerson) >= 0 ||
|
||||
doc.reviewers.findIndex((emp) => emp === currentPerson) >= 0
|
||||
) {
|
||||
docs[doc.attachedTo] = doc
|
||||
|
||||
lastTemplate = template
|
||||
lastSeqNumber = doc.seqNumber
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
document: documents.class.ControlledDocument
|
||||
},
|
||||
sort: {
|
||||
'$lookup.document.template': SortingOrder.Ascending,
|
||||
'$lookup.document.seqNumber': SortingOrder.Ascending,
|
||||
'$lookup.document.major': SortingOrder.Descending,
|
||||
'$lookup.document.minor': SortingOrder.Descending,
|
||||
'$lookup.document.patch': SortingOrder.Descending
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
docsQuery.unsubscribe()
|
||||
}
|
||||
|
||||
let directChildrenDocs: ControlledDocument[] = []
|
||||
$: if (directChildrenMetas !== undefined) {
|
||||
directChildrenDocs = Object.values(docs).filter(
|
||||
(d) => directChildrenMetas !== undefined && directChildrenMetas.findIndex((m) => m.meta === d.attachedTo) >= 0
|
||||
)
|
||||
directChildrenDocs.sort(compareDocs)
|
||||
}
|
||||
|
||||
let parentDocs: ControlledDocument[] = []
|
||||
$: if (meta !== undefined) {
|
||||
parentDocs = [...meta.path]
|
||||
.reverse()
|
||||
.map((mId) => docs[mId])
|
||||
.filter(notEmpty)
|
||||
}
|
||||
|
||||
let levels: Array<[HierarchyDocument[], boolean]> = []
|
||||
let levels: Array<[DocumentBundle[], boolean]> = []
|
||||
$: {
|
||||
levels = []
|
||||
const parents = tree
|
||||
.parentChainOf(docmetaid)
|
||||
.reverse()
|
||||
.map((ref) => tree.bundleOf(ref))
|
||||
.filter((r) => r !== undefined)
|
||||
const me = tree.bundleOf(docmetaid)
|
||||
const children = tree
|
||||
.childrenOf(docmetaid)
|
||||
.map((ref) => tree.bundleOf(ref))
|
||||
.filter((r) => r !== undefined)
|
||||
|
||||
if (parentDocs?.length > 0) {
|
||||
levels.push([parentDocs, false])
|
||||
}
|
||||
|
||||
if (document !== undefined) {
|
||||
levels.push([[document], true])
|
||||
}
|
||||
|
||||
if (directChildrenDocs?.length > 0) {
|
||||
levels.push([directChildrenDocs, false])
|
||||
if (parents.length > 0) levels.push([parents as DocumentBundle[], false])
|
||||
if (me) {
|
||||
levels.push([[me], true])
|
||||
}
|
||||
if (children.length > 0) levels.push([children as DocumentBundle[], false])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if levels.length > 0}
|
||||
{@const [firstDocs, firstHltd] = levels[0]}
|
||||
<div class="root">
|
||||
{#each firstDocs as doc}
|
||||
<DocumentFlatTreeElement {doc} {project} highlighted={firstHltd} />
|
||||
{#each firstDocs as bundle}
|
||||
<DocumentFlatTreeElement {bundle} {project} highlighted={firstHltd} />
|
||||
{/each}
|
||||
{#if levels.length > 1}
|
||||
{@const [secondDocs, secondHltd] = levels[1]}
|
||||
<div class="container">
|
||||
{#each secondDocs as doc}
|
||||
<DocumentFlatTreeElement {doc} {project} highlighted={secondHltd} />
|
||||
{#each secondDocs as bundle}
|
||||
<DocumentFlatTreeElement {bundle} {project} highlighted={secondHltd} />
|
||||
{/each}
|
||||
{#if levels.length > 2}
|
||||
{@const [thirdDocs, thirdHltd] = levels[2]}
|
||||
<div class="container">
|
||||
{#each thirdDocs as doc}
|
||||
<DocumentFlatTreeElement {doc} {project} highlighted={thirdHltd} />
|
||||
{#each thirdDocs as bundle}
|
||||
<DocumentFlatTreeElement {bundle} {project} highlighted={thirdHltd} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -214,5 +101,8 @@
|
||||
padding: 0 1rem;
|
||||
border-left: 2px solid var(--theme-navpanel-border);
|
||||
gap: 0.25rem;
|
||||
|
||||
padding-left: 0.25rem;
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
+14
-5
@@ -14,17 +14,25 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import documents, { getDocumentName, type Document, type Project } from '@hcengineering/controlled-documents'
|
||||
import documents, {
|
||||
DocumentBundle,
|
||||
getDocumentName,
|
||||
isFolder,
|
||||
type Project
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { Icon, navigate } from '@hcengineering/ui'
|
||||
|
||||
import { getProjectDocumentLink } from '../../../../navigation'
|
||||
|
||||
export let doc: Document
|
||||
export let bundle: DocumentBundle
|
||||
export let project: Ref<Project>
|
||||
export let highlighted: boolean = false
|
||||
|
||||
const icon = documents.icon.Document
|
||||
$: meta = bundle?.DocumentMeta[0]
|
||||
$: prjdoc = bundle?.ProjectDocument[0]
|
||||
$: document = bundle?.ControlledDocument[0]
|
||||
$: icon = isFolder(prjdoc) ? documents.icon.Folder : documents.icon.Document
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
@@ -32,7 +40,8 @@
|
||||
<div
|
||||
class="antiNav-element root"
|
||||
on:click={() => {
|
||||
const loc = getProjectDocumentLink(doc, project)
|
||||
if (!document) return
|
||||
const loc = getProjectDocumentLink(document, project)
|
||||
navigate(loc)
|
||||
}}
|
||||
>
|
||||
@@ -48,7 +57,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<span class="an-element__label" class:font-medium={highlighted}>
|
||||
{getDocumentName(doc)}
|
||||
{document ? getDocumentName(document) : meta.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
+24
-112
@@ -13,23 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { type Ref, type Doc, SortingOrder, WithLookup, toIdMap } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { type Action } from '@hcengineering/ui'
|
||||
import documents, {
|
||||
type ControlledDocument,
|
||||
type DocumentMeta,
|
||||
type ProjectMeta,
|
||||
type ProjectDocument,
|
||||
getDocumentName,
|
||||
DocumentState
|
||||
DocumentState,
|
||||
ProjectDocumentTree,
|
||||
getDocumentName
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type Action } from '@hcengineering/ui'
|
||||
import { TreeItem } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let tree = new ProjectDocumentTree()
|
||||
export let documentIds: Ref<DocumentMeta>[] = []
|
||||
|
||||
export let projectMeta: ProjectMeta[] = []
|
||||
export let childrenByParent: Record<Ref<DocumentMeta>, Array<ProjectMeta>>
|
||||
export let selected: Ref<Doc> | undefined
|
||||
export let level: number = 0
|
||||
export let getMoreActions: ((obj: Doc, originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
|
||||
@@ -45,112 +42,27 @@
|
||||
|
||||
import DropArea from './DropArea.svelte'
|
||||
|
||||
const removeStates = [DocumentState.Obsolete, DocumentState.Deleted]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const currentPerson = getCurrentEmployee()
|
||||
|
||||
let docs: WithLookup<ProjectDocument>[] = []
|
||||
|
||||
function sortDocs (meta: ProjectMeta[]): void {
|
||||
const metaById = new Map(meta.map((p) => [p._id, p]))
|
||||
docs = docs.slice().sort((a, b) => {
|
||||
const metaA = metaById.get(a.attachedTo)
|
||||
const metaB = metaById.get(b.attachedTo)
|
||||
|
||||
if (metaA !== undefined && metaB !== undefined) {
|
||||
return metaA.rank.localeCompare(metaB.rank)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
$: sortDocs(projectMeta)
|
||||
|
||||
const docsQuery = createQuery()
|
||||
|
||||
$: docsQuery.query(
|
||||
documents.class.ProjectDocument,
|
||||
{
|
||||
'$lookup.document.state': { $ne: DocumentState.Deleted },
|
||||
attachedTo: { $in: projectMeta.map((p) => p._id) }
|
||||
},
|
||||
(result) => {
|
||||
docs = []
|
||||
let lastTemplate: string | undefined = '###'
|
||||
let lastSeqNumber = -1
|
||||
|
||||
for (const prjdoc of result) {
|
||||
if (prjdoc.document === documents.ids.Folder) {
|
||||
docs.push(prjdoc)
|
||||
continue
|
||||
}
|
||||
|
||||
const doc = prjdoc.$lookup?.document as ControlledDocument | undefined
|
||||
if (doc === undefined) continue
|
||||
if (doc.state === DocumentState.Deleted) continue
|
||||
|
||||
// TODO add proper fix, when document with no template copied, saved value is null
|
||||
const template = doc.template ?? undefined
|
||||
if (template === lastTemplate && doc.seqNumber === lastSeqNumber) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
[DocumentState.Effective, DocumentState.Archived].includes(doc.state) ||
|
||||
doc.owner === currentPerson ||
|
||||
doc.coAuthors.findIndex((emp) => emp === currentPerson) >= 0 ||
|
||||
doc.approvers.findIndex((emp) => emp === currentPerson) >= 0 ||
|
||||
doc.reviewers.findIndex((emp) => emp === currentPerson) >= 0
|
||||
) {
|
||||
docs.push(prjdoc)
|
||||
|
||||
lastTemplate = template
|
||||
lastSeqNumber = doc.seqNumber
|
||||
}
|
||||
}
|
||||
|
||||
sortDocs(projectMeta)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
document: documents.class.ControlledDocument
|
||||
},
|
||||
sort: {
|
||||
'$lookup.document.template': SortingOrder.Ascending,
|
||||
'$lookup.document.seqNumber': SortingOrder.Ascending,
|
||||
'$lookup.document.major': SortingOrder.Descending,
|
||||
'$lookup.document.minor': SortingOrder.Descending,
|
||||
'$lookup.document.patch': SortingOrder.Descending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
let docsMeta: DocumentMeta[] = []
|
||||
|
||||
const metaQuery = createQuery()
|
||||
$: metaQuery.query(documents.class.DocumentMeta, { _id: { $in: projectMeta.map((p) => p.meta) } }, (result) => {
|
||||
docsMeta = result
|
||||
})
|
||||
|
||||
async function getDocMoreActions (obj: Doc): Promise<Action[]> {
|
||||
return getMoreActions !== undefined ? await getMoreActions(obj) : []
|
||||
}
|
||||
|
||||
$: projectMetaById = toIdMap(projectMeta)
|
||||
$: docsMetaById = toIdMap(docsMeta)
|
||||
</script>
|
||||
|
||||
{#each docs as prjdoc}
|
||||
{@const pjmeta = projectMetaById.get(prjdoc.attachedTo)}
|
||||
{@const doc = prjdoc.$lookup?.document}
|
||||
{@const metaid = pjmeta?.meta}
|
||||
{@const meta = metaid ? docsMetaById.get(metaid) : undefined}
|
||||
{#each documentIds as metaid}
|
||||
{@const bundle = tree.bundleOf(metaid)}
|
||||
{@const prjdoc = bundle?.ProjectDocument[0]}
|
||||
{@const doc = bundle?.ControlledDocument[0]}
|
||||
{@const meta = bundle?.DocumentMeta[0]}
|
||||
{@const title = doc ? getDocumentName(doc) : meta?.title ?? ''}
|
||||
{@const docid = doc?._id ?? prjdoc._id}
|
||||
{@const isFolder = prjdoc.document === documents.ids.Folder}
|
||||
{@const isObsolete = doc ? doc.state === DocumentState.Obsolete : false}
|
||||
{@const children = metaid ? childrenByParent[metaid] ?? [] : []}
|
||||
{@const docid = doc?._id ?? prjdoc?._id}
|
||||
{@const isFolder = prjdoc?.document === documents.ids.Folder}
|
||||
{@const children = tree.childrenOf(metaid)}
|
||||
{@const isRemoved = doc && removeStates.includes(doc.state)}
|
||||
|
||||
{#if metaid && (!isObsolete || children.length > 0)}
|
||||
{#if prjdoc && metaid}
|
||||
{@const isDraggedOver = draggedOver === metaid}
|
||||
<div class="flex-col relative">
|
||||
{#if isDraggedOver}
|
||||
@@ -160,7 +72,7 @@
|
||||
_id={docid}
|
||||
icon={isFolder ? documents.icon.Folder : documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: isObsolete ? 'var(--dangerous-bg-color)' : 'currentColor'
|
||||
fill: isRemoved ? 'var(--dangerous-bg-color)' : 'currentColor'
|
||||
}}
|
||||
{title}
|
||||
selected={selected === docid || selected === prjdoc._id}
|
||||
@@ -190,8 +102,8 @@
|
||||
<svelte:fragment slot="dropbox">
|
||||
{#if children.length}
|
||||
<svelte:self
|
||||
projectMeta={children}
|
||||
{childrenByParent}
|
||||
documentIds={children}
|
||||
{tree}
|
||||
{selected}
|
||||
{collapsedPrefix}
|
||||
{getMoreActions}
|
||||
|
||||
+16
-29
@@ -13,20 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { WithLookup, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getPlatformColorForTextDef, themeStore, getTreeCollapsed } from '@hcengineering/ui'
|
||||
import documents, {
|
||||
type DocumentMeta,
|
||||
type DocumentSpace,
|
||||
type Project,
|
||||
type ProjectMeta
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import documents, { ProjectDocumentTree, type DocumentSpace, type Project } from '@hcengineering/controlled-documents'
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { getPlatformColorForTextDef, themeStore } from '@hcengineering/ui'
|
||||
import { TreeNode } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { createDocumentHierarchyQuery } from '../../utils'
|
||||
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
|
||||
import { getProjectDocsHierarchy } from '../../utils'
|
||||
|
||||
export let space: DocumentSpace
|
||||
export let project: Ref<Project> | undefined
|
||||
@@ -34,24 +28,17 @@
|
||||
export let collapsedPrefix: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
// let collapsed: boolean = getPrefixedTreeCollapsed(space._id, collapsedPrefix)
|
||||
// $: setPrefixedTreeCollapsed(space._id, collapsedPrefix, collapsed)
|
||||
// $: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
|
||||
|
||||
let rootDocs: Array<WithLookup<ProjectMeta>> = []
|
||||
let childrenByParent: Record<Ref<DocumentMeta>, Array<WithLookup<ProjectMeta>>> = {}
|
||||
let tree = new ProjectDocumentTree()
|
||||
|
||||
const docsQuery = createQuery()
|
||||
$: docsQuery.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
space: space._id,
|
||||
project
|
||||
},
|
||||
(result) => {
|
||||
;({ rootDocs, childrenByParent } = getProjectDocsHierarchy(result))
|
||||
}
|
||||
)
|
||||
const query = createDocumentHierarchyQuery()
|
||||
$: if (document !== undefined && project !== undefined) {
|
||||
query.query(space._id, project, (data) => {
|
||||
tree = data
|
||||
})
|
||||
}
|
||||
|
||||
$: root = tree.childrenOf(documents.ids.NoParent)
|
||||
</script>
|
||||
|
||||
<TreeNode
|
||||
@@ -63,12 +50,12 @@
|
||||
title={space.name}
|
||||
highlighted={selected !== undefined}
|
||||
selected={selected === undefined}
|
||||
empty={rootDocs.length === 0}
|
||||
empty={root.length === 0}
|
||||
{collapsedPrefix}
|
||||
type={'nested-selectable'}
|
||||
on:click={() => {
|
||||
dispatch('selected', space)
|
||||
}}
|
||||
>
|
||||
<DocHierarchyLevel projectMeta={rootDocs} {childrenByParent} {selected} {collapsedPrefix} on:selected />
|
||||
<DocHierarchyLevel documentIds={root} {tree} {selected} {collapsedPrefix} on:selected />
|
||||
</TreeNode>
|
||||
|
||||
+38
-67
@@ -13,20 +13,6 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import {
|
||||
type Action,
|
||||
getPlatformColorForTextDef,
|
||||
themeStore,
|
||||
navigate,
|
||||
IconEdit,
|
||||
Label,
|
||||
closeTooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { getActions as getContributedActions, TreeNode, TreeItem } from '@hcengineering/view-resources'
|
||||
import { ActionGroup } from '@hcengineering/view'
|
||||
import {
|
||||
type ControlledDocument,
|
||||
type DocumentMeta,
|
||||
@@ -34,27 +20,41 @@
|
||||
type DocumentSpaceType,
|
||||
type Project,
|
||||
type ProjectDocument,
|
||||
type ProjectMeta,
|
||||
ProjectDocumentTree,
|
||||
getDocumentName
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { type Doc, type Ref, type Space, WithLookup } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
type Action,
|
||||
IconEdit,
|
||||
Label,
|
||||
closeTooltip,
|
||||
getPlatformColorForTextDef,
|
||||
navigate,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { ActionGroup } from '@hcengineering/view'
|
||||
import { TreeItem, TreeNode, getActions as getContributedActions } from '@hcengineering/view-resources'
|
||||
|
||||
import ProjectSelector from '../project/ProjectSelector.svelte'
|
||||
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
|
||||
import { getDocumentIdFromFragment, getProjectDocumentLink } from '../../navigation'
|
||||
import {
|
||||
canCreateChildDocument,
|
||||
canCreateChildFolder,
|
||||
createDocument,
|
||||
createDocumentHierarchyQuery,
|
||||
createFolder,
|
||||
getCurrentProject,
|
||||
getLatestProjectId,
|
||||
setCurrentProject,
|
||||
getProjectDocsHierarchy,
|
||||
isEditableProject,
|
||||
createDocument,
|
||||
canCreateChildDocument,
|
||||
moveDocument,
|
||||
moveDocumentBefore,
|
||||
moveDocumentAfter,
|
||||
canCreateChildFolder,
|
||||
createFolder
|
||||
moveDocumentBefore,
|
||||
setCurrentProject
|
||||
} from '../../utils'
|
||||
import ProjectSelector from '../project/ProjectSelector.svelte'
|
||||
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
|
||||
|
||||
import documents from '../../plugin'
|
||||
|
||||
@@ -83,27 +83,14 @@
|
||||
let project: Ref<Project> = documents.ids.NoProject
|
||||
$: void selectProject(space)
|
||||
|
||||
let docsByMeta = new Map<Ref<DocumentMeta>, WithLookup<ProjectMeta>>()
|
||||
let rootDocs: Array<WithLookup<ProjectMeta>> = []
|
||||
let childrenByParent: Record<Ref<DocumentMeta>, Array<WithLookup<ProjectMeta>>> = {}
|
||||
let tree = new ProjectDocumentTree()
|
||||
|
||||
const projectMetaQ = createQuery()
|
||||
$: projectMetaQ.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
space: space._id,
|
||||
project
|
||||
},
|
||||
(result) => {
|
||||
docsByMeta = new Map(result.map((r) => [r.meta, r]))
|
||||
;({ rootDocs, childrenByParent } = getProjectDocsHierarchy(result))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
meta: documents.class.DocumentMeta
|
||||
}
|
||||
}
|
||||
)
|
||||
const query = createDocumentHierarchyQuery()
|
||||
$: if (document !== undefined && project !== undefined) {
|
||||
query.query(space._id, project, (data) => {
|
||||
tree = data
|
||||
})
|
||||
}
|
||||
|
||||
let selectedControlledDoc: ControlledDocument | undefined = undefined
|
||||
|
||||
@@ -134,23 +121,6 @@
|
||||
selectedControlledDoc = undefined
|
||||
}
|
||||
|
||||
function getAllDescendants (obj: Ref<DocumentMeta>): Ref<DocumentMeta>[] {
|
||||
const result: Ref<DocumentMeta>[] = []
|
||||
const queue: Ref<DocumentMeta>[] = [obj]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue.pop()
|
||||
if (next === undefined) break
|
||||
|
||||
const children = childrenByParent[next] ?? []
|
||||
const childrenRefs = children.map((p) => p.meta)
|
||||
result.push(...childrenRefs)
|
||||
queue.push(...childrenRefs)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function selectProject (space: DocumentSpace): Promise<void> {
|
||||
project = getCurrentProject(space._id) ?? (await getLatestProjectId(space._id, true)) ?? documents.ids.NoProject
|
||||
}
|
||||
@@ -253,7 +223,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
cannotDropTo = [object, ...getAllDescendants(object)]
|
||||
cannotDropTo = [object, ...tree.descendantsOf(object)]
|
||||
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
@@ -311,8 +281,8 @@
|
||||
return
|
||||
}
|
||||
if (draggedItem !== undefined && canDrop(draggedItem, object)) {
|
||||
const doc = docsByMeta.get(draggedItem)
|
||||
const target = docsByMeta.get(object)
|
||||
const doc = tree.bundleOf(draggedItem)?.ProjectMeta[0]
|
||||
const target = tree.bundleOf(object)?.ProjectMeta[0]
|
||||
|
||||
if (doc !== undefined && target !== undefined && doc._id !== target._id) {
|
||||
if (object === documents.ids.NoParent) {
|
||||
@@ -385,10 +355,11 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if rootDocs.length > 0}
|
||||
{@const root = tree.childrenOf(documents.ids.NoParent)}
|
||||
{#if root.length > 0}
|
||||
<DocHierarchyLevel
|
||||
projectMeta={rootDocs}
|
||||
{childrenByParent}
|
||||
{tree}
|
||||
documentIds={root}
|
||||
{selected}
|
||||
getMoreActions={getDocumentActions}
|
||||
on:selected={(e) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,9 +28,12 @@ import documents, {
|
||||
type ProjectDocument,
|
||||
type ProjectMeta,
|
||||
ControlledDocumentState,
|
||||
type DocumentBundle,
|
||||
DocumentState,
|
||||
emptyBundle,
|
||||
getDocumentName,
|
||||
getFirstRank
|
||||
getFirstRank,
|
||||
ProjectDocumentTree
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import core, {
|
||||
type Class,
|
||||
@@ -49,7 +52,7 @@ import core, {
|
||||
checkPermission
|
||||
} from '@hcengineering/core'
|
||||
import { type IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import request, { type Request, RequestStatus } from '@hcengineering/request'
|
||||
import { isEmptyMarkup } from '@hcengineering/text'
|
||||
import { type Location, getUserTimezone, showPopup } from '@hcengineering/ui'
|
||||
@@ -854,3 +857,53 @@ export async function moveDocumentAfter (doc: ProjectMeta, after: ProjectMeta):
|
||||
|
||||
await client.update(doc, { parent, path, rank })
|
||||
}
|
||||
|
||||
export class DocumentHiearchyQuery {
|
||||
queries = {
|
||||
prjMeta: createQuery(),
|
||||
prjDoc: createQuery()
|
||||
}
|
||||
|
||||
bundle: DocumentBundle = { ...emptyBundle() }
|
||||
|
||||
handleUpdate (data: Partial<DocumentBundle>, callback: (tree: ProjectDocumentTree) => void): void {
|
||||
this.bundle = { ...this.bundle, ...data }
|
||||
callback(new ProjectDocumentTree(this.bundle))
|
||||
}
|
||||
|
||||
query (
|
||||
space: Ref<DocumentSpace>,
|
||||
project: Ref<Project<DocumentSpace>>,
|
||||
callback: (tree: ProjectDocumentTree) => void
|
||||
): void {
|
||||
project = project ?? documents.ids.NoProject
|
||||
|
||||
this.queries.prjMeta.query(
|
||||
documents.class.ProjectMeta,
|
||||
{ space, project },
|
||||
(ProjectMeta) => {
|
||||
const DocumentMeta = ProjectMeta.map((e) => e.$lookup?.meta).filter((e) => e !== undefined) as DocumentMeta[]
|
||||
const patch: Partial<DocumentBundle> = { ProjectMeta, DocumentMeta }
|
||||
this.handleUpdate(patch, callback)
|
||||
},
|
||||
{ lookup: { meta: documents.class.DocumentMeta } }
|
||||
)
|
||||
|
||||
this.queries.prjDoc.query(
|
||||
documents.class.ProjectDocument,
|
||||
{ space, project },
|
||||
(ProjectDocument) => {
|
||||
const ControlledDocument = ProjectDocument.map((e) => e.$lookup?.document as ControlledDocument).filter(
|
||||
(e) => e !== undefined
|
||||
)
|
||||
const patch: Partial<DocumentBundle> = { ProjectDocument, ControlledDocument }
|
||||
this.handleUpdate(patch, callback)
|
||||
},
|
||||
{ lookup: { document: documents.class.ControlledDocument } }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function createDocumentHierarchyQuery (): DocumentHiearchyQuery {
|
||||
return new DocumentHiearchyQuery()
|
||||
}
|
||||
|
||||
@@ -20,11 +20,17 @@ import {
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
Hierarchy,
|
||||
getCurrentAccount,
|
||||
Rank,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
TxOperations
|
||||
Timestamp,
|
||||
toIdMap,
|
||||
TxOperations,
|
||||
type SocialId,
|
||||
type PersonId
|
||||
} from '@hcengineering/core'
|
||||
import { LexoDecimal, LexoNumeralSystem36, LexoRank } from 'lexorank'
|
||||
import LexoRankBucket from 'lexorank/lib/lexoRank/lexoRankBucket'
|
||||
@@ -33,6 +39,8 @@ import documents from './plugin'
|
||||
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import { Person, Employee, getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import tags, { TagReference } from '@hcengineering/tags'
|
||||
import {
|
||||
ChangeControl,
|
||||
@@ -47,7 +55,6 @@ import {
|
||||
ProjectDocument,
|
||||
ProjectMeta
|
||||
} from './types'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -138,30 +145,152 @@ export async function deleteProjectDrafts (client: ApplyOperations, source: Ref<
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectDocumentTree {
|
||||
rootDocs: ProjectMeta[]
|
||||
childrenByParent: Map<Ref<DocumentMeta>, ProjectMeta[]>
|
||||
export function isCollaborator (doc: ControlledDocument, person: Ref<Employee>): boolean {
|
||||
return (
|
||||
doc.owner === person ||
|
||||
doc.coAuthors.includes(person) ||
|
||||
doc.approvers.includes(person) ||
|
||||
doc.reviewers.includes(person)
|
||||
)
|
||||
}
|
||||
|
||||
constructor (pjMeta: ProjectMeta[]) {
|
||||
this.rootDocs = []
|
||||
this.childrenByParent = new Map<Ref<DocumentMeta>, Array<ProjectMeta>>()
|
||||
export function isFolder (doc: ProjectDocument | undefined): boolean {
|
||||
return doc !== undefined && doc.document === documents.ids.Folder
|
||||
}
|
||||
|
||||
for (const meta of pjMeta) {
|
||||
const parentId = meta.path[0] ?? documents.ids.NoParent
|
||||
function extractPresentableStateFromDocumentBundle (bundle: DocumentBundle, prjmeta: ProjectMeta): DocumentBundle {
|
||||
bundle = { ...bundle }
|
||||
|
||||
if (!this.childrenByParent.has(parentId)) {
|
||||
this.childrenByParent.set(parentId, [])
|
||||
const person = getCurrentEmployee()
|
||||
const documentById = toIdMap(bundle.ControlledDocument)
|
||||
|
||||
const getSortSequence = (prjdoc: ProjectDocument): number[] => {
|
||||
const doc = documentById.get(prjdoc.document as Ref<ControlledDocument>)
|
||||
return doc !== undefined ? [doc.seqNumber, doc.major, doc.minor, doc.createdOn ?? 0] : [0, 0, 0, 0]
|
||||
}
|
||||
|
||||
const prjdoc = bundle.ProjectDocument.filter((prjdoc) => {
|
||||
if (prjdoc.attachedTo !== prjmeta._id) return false
|
||||
if (isFolder(prjdoc)) return true
|
||||
const doc = documentById.get(prjdoc.document as Ref<ControlledDocument>)
|
||||
const isPublicState = doc?.state === DocumentState.Effective || doc?.state === DocumentState.Archived
|
||||
return doc !== undefined && (isPublicState || isCollaborator(doc, person))
|
||||
}).sort((a, b) => {
|
||||
const s0 = getSortSequence(a)
|
||||
const s1 = getSortSequence(b)
|
||||
return s0.reduce((r, v, i) => (r !== 0 ? r : s1[i] - v), 0)
|
||||
})[0]
|
||||
|
||||
const doc = prjdoc !== undefined ? documentById.get(prjdoc.document as Ref<ControlledDocument>) : undefined
|
||||
|
||||
bundle.ProjectMeta = [prjmeta]
|
||||
bundle.ProjectDocument = prjdoc !== undefined ? [prjdoc] : []
|
||||
bundle.ControlledDocument = doc !== undefined ? [doc] : []
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
export class ProjectDocumentTree {
|
||||
parents: Map<Ref<DocumentMeta>, Ref<DocumentMeta>>
|
||||
nodesChildren: Map<Ref<DocumentMeta>, DocumentBundle[]>
|
||||
nodes: Map<Ref<DocumentMeta>, DocumentBundle>
|
||||
links: Map<Ref<Doc>, Ref<DocumentMeta>>
|
||||
|
||||
constructor (bundle?: DocumentBundle) {
|
||||
bundle = { ...emptyBundle(), ...bundle }
|
||||
const { bundles, links } = compileBundles(bundle)
|
||||
this.links = links
|
||||
this.nodes = new Map()
|
||||
this.nodesChildren = new Map()
|
||||
this.parents = new Map()
|
||||
|
||||
bundles.sort((a, b) => {
|
||||
const rankA = a.ProjectMeta[0]?.rank ?? ''
|
||||
const rankB = b.ProjectMeta[0]?.rank ?? ''
|
||||
return rankA.localeCompare(rankB)
|
||||
})
|
||||
|
||||
for (const bundle of bundles) {
|
||||
const prjmeta = bundle.ProjectMeta[0]
|
||||
if (prjmeta === undefined) continue
|
||||
|
||||
const presentable = extractPresentableStateFromDocumentBundle(bundle, prjmeta)
|
||||
this.nodes.set(prjmeta.meta, presentable)
|
||||
|
||||
const parent = prjmeta.path[0] ?? documents.ids.NoParent
|
||||
this.parents.set(prjmeta.meta, parent)
|
||||
|
||||
if (!this.nodesChildren.has(parent)) {
|
||||
this.nodesChildren.set(parent, [])
|
||||
}
|
||||
this.nodesChildren.get(parent)?.push(bundle)
|
||||
}
|
||||
|
||||
this.childrenByParent.get(parentId)?.push(meta)
|
||||
const nodesForRemoval = new Set<Ref<DocumentMeta>>()
|
||||
for (const [id, node] of this.nodes) {
|
||||
const state = node.ControlledDocument[0]?.state
|
||||
const isRemoved = state === DocumentState.Obsolete || state === DocumentState.Deleted
|
||||
if (isRemoved) nodesForRemoval.add(id)
|
||||
}
|
||||
|
||||
if (parentId === documents.ids.NoParent) {
|
||||
this.rootDocs.push(meta)
|
||||
}
|
||||
for (const id of this.nodes.keys()) {
|
||||
if (!nodesForRemoval.has(id)) continue
|
||||
|
||||
const blocked = this.descendantsOf(id).some((node) => !nodesForRemoval.has(node))
|
||||
if (blocked) nodesForRemoval.delete(id)
|
||||
}
|
||||
|
||||
for (const id of nodesForRemoval) {
|
||||
this.nodes.delete(id)
|
||||
this.parents.delete(id)
|
||||
this.nodesChildren.delete(id)
|
||||
}
|
||||
|
||||
for (const [id, children] of this.nodesChildren) {
|
||||
this.nodesChildren.set(
|
||||
id,
|
||||
children.filter((c) => !nodesForRemoval.has(c.ProjectMeta[0].meta))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getDescendants (parent: Ref<DocumentMeta>): Ref<DocumentMeta>[] {
|
||||
metaOf (ref: Ref<Doc> | undefined): Ref<DocumentMeta> | undefined {
|
||||
if (ref === undefined) return
|
||||
return this.links.get(ref)
|
||||
}
|
||||
|
||||
parentChainOf (ref: Ref<DocumentMeta> | undefined): Ref<DocumentMeta>[] {
|
||||
if (ref === undefined) return []
|
||||
// Found a bug that can cause path field to contain invalid state,
|
||||
// until we fix it with migration and a separate fix it's better to use parent.
|
||||
//
|
||||
// return this.bundleOf(ref)?.ProjectMeta[0]?.path ?? []
|
||||
const parents: Ref<DocumentMeta>[] = []
|
||||
while (this.parentOf(ref) !== documents.ids.NoParent) {
|
||||
ref = this.parentOf(ref)
|
||||
parents.push(ref)
|
||||
}
|
||||
return parents
|
||||
}
|
||||
|
||||
parentOf (ref: Ref<DocumentMeta> | undefined): Ref<DocumentMeta> {
|
||||
if (ref === undefined) {
|
||||
return documents.ids.NoParent
|
||||
}
|
||||
return this.parents.get(ref) ?? documents.ids.NoParent
|
||||
}
|
||||
|
||||
bundleOf (ref: Ref<DocumentMeta> | undefined): DocumentBundle | undefined {
|
||||
if (ref === undefined) return
|
||||
return this.nodes.get(ref)
|
||||
}
|
||||
|
||||
childrenOf (ref: Ref<DocumentMeta> | undefined): Ref<DocumentMeta>[] {
|
||||
if (ref === undefined) return []
|
||||
return this.nodesChildren.get(ref)?.map((p) => p.ProjectMeta[0].meta) ?? []
|
||||
}
|
||||
|
||||
descendantsOf (parent: Ref<DocumentMeta>): Ref<DocumentMeta>[] {
|
||||
const result: Ref<DocumentMeta>[] = []
|
||||
const queue: Ref<DocumentMeta>[] = [parent]
|
||||
|
||||
@@ -169,8 +298,8 @@ class ProjectDocumentTree {
|
||||
const next = queue.pop()
|
||||
if (next === undefined) break
|
||||
|
||||
const children = this.childrenByParent.get(next) ?? []
|
||||
const childrenRefs = children.map((p) => p.meta)
|
||||
const children = this.nodesChildren.get(next) ?? []
|
||||
const childrenRefs = children.map((p) => p.ProjectMeta[0].meta)
|
||||
result.push(...childrenRefs)
|
||||
queue.push(...childrenRefs)
|
||||
}
|
||||
@@ -184,8 +313,8 @@ export async function findProjectDocsHierarchy (
|
||||
space: Ref<DocumentSpace>,
|
||||
project?: Ref<Project<DocumentSpace>>
|
||||
): Promise<ProjectDocumentTree> {
|
||||
const pjMeta = await client.findAll(documents.class.ProjectMeta, { space, project })
|
||||
return new ProjectDocumentTree(pjMeta)
|
||||
const ProjectMeta = await client.findAll(documents.class.ProjectMeta, { space, project })
|
||||
return new ProjectDocumentTree({ ...emptyBundle(), ProjectMeta })
|
||||
}
|
||||
|
||||
export interface DocumentBundle {
|
||||
@@ -201,7 +330,7 @@ export interface DocumentBundle {
|
||||
Attachment: Attachment[]
|
||||
}
|
||||
|
||||
function emptyBundle (): DocumentBundle {
|
||||
export function emptyBundle (): DocumentBundle {
|
||||
return {
|
||||
DocumentMeta: [],
|
||||
ProjectMeta: [],
|
||||
@@ -216,6 +345,46 @@ function emptyBundle (): DocumentBundle {
|
||||
}
|
||||
}
|
||||
|
||||
export function compileBundles (all: DocumentBundle): {
|
||||
bundles: DocumentBundle[]
|
||||
links: Map<Ref<Doc>, Ref<DocumentMeta>>
|
||||
} {
|
||||
const bundles = new Map<Ref<DocumentMeta>, DocumentBundle>(all.DocumentMeta.map((m) => [m._id, { ...emptyBundle() }]))
|
||||
const links = new Map<Ref<Doc>, Ref<DocumentMeta>>()
|
||||
|
||||
const link = (ref: Ref<Doc>, lookup: Ref<Doc>): void => {
|
||||
const meta = links.get(lookup)
|
||||
if (meta !== undefined) links.set(ref, meta)
|
||||
}
|
||||
|
||||
const relink = (ref: Ref<Doc>, prop: keyof DocumentBundle, obj: DocumentBundle[typeof prop][0]): void => {
|
||||
const meta = links.get(ref)
|
||||
if (meta !== undefined) bundles.get(meta)?.[prop].push(obj as any)
|
||||
}
|
||||
|
||||
for (const m of all.DocumentMeta) links.set(m._id, m._id) // DocumentMeta -> DocumentMeta
|
||||
for (const m of all.ProjectMeta) links.set(m._id, m.meta) // ProjectMeta -> DocumentMeta
|
||||
for (const m of all.ProjectDocument) {
|
||||
link(m._id, m.attachedTo) // ProjectDocument -> ProjectMeta
|
||||
link(m.document, m.attachedTo) // ControlledDocument -> ProjectMeta
|
||||
}
|
||||
for (const m of all.ControlledDocument) link(m.changeControl, m.attachedTo) // ChangeControl -> ControlledDocument
|
||||
for (const m of all.DocumentRequest) link(m._id, m.attachedTo) // DocumentRequest -> ControlledDocument
|
||||
for (const m of all.DocumentSnapshot) link(m._id, m.attachedTo) // DocumentSnapshot -> ControlledDocument
|
||||
for (const m of all.ChatMessage) link(m._id, m.attachedTo) // ChatMessage -> (ControlledDocument | ChatMessage)
|
||||
for (const m of all.TagReference) link(m._id, m.attachedTo) // TagReference -> ControlledDocument
|
||||
for (const m of all.Attachment) link(m._id, m.attachedTo) // Attachment -> (ControlledDocument | ChatMessage)
|
||||
|
||||
let key: keyof DocumentBundle
|
||||
for (key in all) {
|
||||
all[key].forEach((value) => {
|
||||
relink(value._id, key, value)
|
||||
})
|
||||
}
|
||||
|
||||
return { bundles: Array.from(bundles.values()), links }
|
||||
}
|
||||
|
||||
export async function findAllDocumentBundles (
|
||||
client: TxOperations,
|
||||
ids: Ref<DocumentMeta>[]
|
||||
@@ -293,40 +462,7 @@ export async function findAllDocumentBundles (
|
||||
...all.ControlledDocument.map((p) => p._id)
|
||||
])
|
||||
|
||||
const bundles = new Map<Ref<DocumentMeta>, DocumentBundle>(all.DocumentMeta.map((m) => [m._id, { ...emptyBundle() }]))
|
||||
const links = new Map<Ref<Doc>, Ref<DocumentMeta>>()
|
||||
|
||||
const link = (ref: Ref<Doc>, lookup: Ref<Doc>): void => {
|
||||
const meta = links.get(lookup)
|
||||
if (meta !== undefined) links.set(ref, meta)
|
||||
}
|
||||
|
||||
const relink = (ref: Ref<Doc>, prop: keyof DocumentBundle, obj: DocumentBundle[typeof prop][0]): void => {
|
||||
const meta = links.get(ref)
|
||||
if (meta !== undefined) bundles.get(meta)?.[prop].push(obj as any)
|
||||
}
|
||||
|
||||
for (const m of all.DocumentMeta) links.set(m._id, m._id) // DocumentMeta -> DocumentMeta
|
||||
for (const m of all.ProjectMeta) links.set(m._id, m.meta) // ProjectMeta -> DocumentMeta
|
||||
for (const m of all.ProjectDocument) {
|
||||
link(m._id, m.attachedTo) // ProjectDocument -> ProjectMeta
|
||||
link(m.document, m.attachedTo) // ControlledDocument -> ProjectMeta
|
||||
}
|
||||
for (const m of all.ControlledDocument) link(m.changeControl, m.attachedTo) // ChangeControl -> ControlledDocument
|
||||
for (const m of all.DocumentRequest) link(m._id, m.attachedTo) // DocumentRequest -> ControlledDocument
|
||||
for (const m of all.DocumentSnapshot) link(m._id, m.attachedTo) // DocumentSnapshot -> ControlledDocument
|
||||
for (const m of all.ChatMessage) link(m._id, m.attachedTo) // ChatMessage -> (ControlledDocument | ChatMessage)
|
||||
for (const m of all.TagReference) link(m._id, m.attachedTo) // TagReference -> ControlledDocument
|
||||
for (const m of all.Attachment) link(m._id, m.attachedTo) // Attachment -> (ControlledDocument | ChatMessage)
|
||||
|
||||
let key: keyof DocumentBundle
|
||||
for (key in all) {
|
||||
all[key].forEach((value) => {
|
||||
relink(value._id, key, value)
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(bundles.values())
|
||||
return compileBundles(all).bundles
|
||||
}
|
||||
|
||||
export async function findOneDocumentBundle (
|
||||
@@ -369,7 +505,7 @@ async function _buildDocumentTransferContext (
|
||||
|
||||
const docIds = new Set<Ref<DocumentMeta>>(request.sourceDocumentIds)
|
||||
for (const id of request.sourceDocumentIds) {
|
||||
sourceTree.getDescendants(id).forEach((d) => docIds.add(d))
|
||||
sourceTree.descendantsOf(id).forEach((d) => docIds.add(d))
|
||||
}
|
||||
|
||||
const bundles = await findAllDocumentBundles(client, Array.from(docIds))
|
||||
@@ -496,6 +632,139 @@ async function _transferDocuments (
|
||||
return commit.result
|
||||
}
|
||||
|
||||
export interface DocumentApprovalState {
|
||||
person?: Ref<Person>
|
||||
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: PersonId) => Ref<Person> | undefined
|
||||
): Map<Ref<ControlledDocument>, DocumentValidationState[]> {
|
||||
const result: ReturnType<typeof extractValidationWorkflow> = 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 ?? m.modifiedBy) === 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) ?? 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
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Jazyk",
|
||||
"Kick": "Vyhodit",
|
||||
"WithAudio": "Zahrnout systémový zvuk",
|
||||
"ShareWithAudioTooltip": "Sdílejte obrazovku se systémovým zvukem. Restartujte sdílení obrazovky, aby se změny projevily."
|
||||
"ShareWithAudioTooltip": "Sdílejte obrazovku se systémovým zvukem. Restartujte sdílení obrazovky, aby se změny projevily.",
|
||||
"MicPermission": "Mikrofon nebyl nalezen, zkontrolujte oprávnění prohlížeče",
|
||||
"CamPermission": "Kamera nebyla nalezena, zkontrolujte oprávnění prohlížeče"
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,8 @@
|
||||
"StartWithRecording": "Mit Aufnahme starten",
|
||||
"Language": "Sprache",
|
||||
"WithAudio": "Systemaudio einschließen",
|
||||
"ShareWithAudioTooltip": "Teile deinen Bildschirm mit Systemaudio. Starte die Bildschirmfreigabe neu, um Änderungen anzuwenden."
|
||||
"ShareWithAudioTooltip": "Teile deinen Bildschirm mit Systemaudio. Starte die Bildschirmfreigabe neu, um Änderungen anzuwenden.",
|
||||
"MicPermission": "Mikrofon nicht gefunden, überprüfen Sie die Browserberechtigungen",
|
||||
"CamPermission": "Kamera nicht gefunden, überprüfen Sie die Browserberechtigungen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Language",
|
||||
"Kick": "Kick",
|
||||
"WithAudio": "Include system audio",
|
||||
"ShareWithAudioTooltip": "Share your screen with system audio. Restart screen share to apply changes."
|
||||
"ShareWithAudioTooltip": "Share your screen with system audio. Restart screen share to apply changes.",
|
||||
"MicPermission": "Microphone not found, check browser permissions",
|
||||
"CamPermission": "Camera not found, check browser permissions"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Idioma",
|
||||
"Kick": "Expulsar",
|
||||
"WithAudio": "Incluir audio del sistema",
|
||||
"ShareWithAudioTooltip": "Comparte tu pantalla con el audio del sistema. Reinicia la pantalla compartida para aplicar los cambios."
|
||||
"ShareWithAudioTooltip": "Comparte tu pantalla con el audio del sistema. Reinicia la pantalla compartida para aplicar los cambios.",
|
||||
"MicPermission": "Micrófono no encontrado, comprueba los permisos del navegador",
|
||||
"CamPermission": "Cámara no encontrada, comprueba los permisos del navegador"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Langue",
|
||||
"Kick": "Expulser",
|
||||
"WithAudio": "Inclure l'audio du système",
|
||||
"ShareWithAudioTooltip": "Partagez votre écran avec l'audio du système. Redémarrez le partage d’écran pour appliquer les modifications."
|
||||
"ShareWithAudioTooltip": "Partagez votre écran avec l'audio du système. Redémarrez le partage d’écran pour appliquer les modifications.",
|
||||
"MicPermission": "Microphone non trouvé, vérifiez les autorisations du navigateur",
|
||||
"CamPermission": "Caméra non trouvée, vérifiez les autorisations du navigateur"
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Lingua",
|
||||
"Kick": "Espellere",
|
||||
"WithAudio": "Includi l'audio di sistema",
|
||||
"ShareWithAudioTooltip": "Condividi lo schermo con l'audio di sistema. Riavvia la condivisione dello schermo per applicare le modifiche."
|
||||
"ShareWithAudioTooltip": "Condividi lo schermo con l'audio di sistema. Riavvia la condivisione dello schermo per applicare le modifiche.",
|
||||
"MicPermission": "Microfono non trovato, controlla le autorizzazioni del browser",
|
||||
"CamPermission": "Camera non trovata, controlla le autorizzazioni del browser"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Idioma",
|
||||
"Kick": "Expulsar",
|
||||
"WithAudio": "Incluir áudio do sistema",
|
||||
"ShareWithAudioTooltip": "Compartilhe sua tela com o áudio do sistema. Reinicie o compartilhamento de tela para aplicar as alterações."
|
||||
"ShareWithAudioTooltip": "Compartilhe sua tela com o áudio do sistema. Reinicie o compartilhamento de tela para aplicar as alterações.",
|
||||
"MicPermission": "Microfone não encontrado, verifique as permissões do navegador",
|
||||
"CamPermission": "Câmera não encontrada, verifique as permissões do navegador"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "Язык",
|
||||
"Kick": "Выгнать",
|
||||
"WithAudio": "С системным звуком",
|
||||
"ShareWithAudioTooltip": "Делитесь экраном с системным звуком. Перезапустите трансляцию, чтобы применить изменения."
|
||||
"ShareWithAudioTooltip": "Делитесь экраном с системным звуком. Перезапустите трансляцию, чтобы применить изменения.",
|
||||
"MicPermission": "Микрофон не найден, проверьте разрешения браузера",
|
||||
"CamPermission": "Камера не найдена, проверьте разрешения браузера"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
"Language": "语言",
|
||||
"Kick": "踢出",
|
||||
"WithAudio": "包含系统音频",
|
||||
"ShareWithAudioTooltip": "与系统音频一起共享屏幕。重新启动屏幕共享以应用更改。"
|
||||
"ShareWithAudioTooltip": "与系统音频一起共享屏幕。重新启动屏幕共享以应用更改。",
|
||||
"MicPermission": "未找到麦克风,请检查浏览器权限",
|
||||
"CamPermission": "未找到摄像头,请检查浏览器权限"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,11 @@
|
||||
import love from '../plugin'
|
||||
import { currentRoom, myInfo, myOffice } from '../stores'
|
||||
import {
|
||||
isCamAllowed,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isFullScreen,
|
||||
isMicAllowed,
|
||||
isMicEnabled,
|
||||
isRecording,
|
||||
isRecordingAvailable,
|
||||
@@ -64,6 +66,7 @@
|
||||
import RoomLanguageSelector from './RoomLanguageSelector.svelte'
|
||||
import RoomModal from './RoomModal.svelte'
|
||||
import ShareSettingPopup from './ShareSettingPopup.svelte'
|
||||
import { Room as LKRoom } from 'livekit-client'
|
||||
|
||||
export let room: Room
|
||||
export let canMaximize: boolean = true
|
||||
@@ -201,8 +204,12 @@
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
showTooltip={{ label: $isMicEnabled ? love.string.Mute : love.string.UnMute, keys: micKeys }}
|
||||
showTooltip={{
|
||||
label: !$isMicAllowed ? love.string.MicPermission : $isMicEnabled ? love.string.Mute : love.string.UnMute,
|
||||
keys: micKeys
|
||||
}}
|
||||
action={changeMute}
|
||||
disabled={!$isMicAllowed}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={micSettings}
|
||||
separate
|
||||
@@ -211,8 +218,15 @@
|
||||
<SplitButton
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
showTooltip={{ label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo, keys: camKeys }}
|
||||
disabled={!$isConnected}
|
||||
showTooltip={{
|
||||
label: !$isCamAllowed
|
||||
? love.string.CamPermission
|
||||
: $isCameraEnabled
|
||||
? love.string.StopVideo
|
||||
: love.string.StartVideo,
|
||||
keys: camKeys
|
||||
}}
|
||||
disabled={!$isCamAllowed}
|
||||
action={changeCam}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={camSettings}
|
||||
|
||||
@@ -51,8 +51,10 @@
|
||||
import {
|
||||
endMeeting,
|
||||
getRoomName,
|
||||
isCamAllowed,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicAllowed,
|
||||
isMicEnabled,
|
||||
isShareWithSound,
|
||||
isSharingEnabled,
|
||||
@@ -197,7 +199,10 @@
|
||||
size={'large'}
|
||||
icon={$isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
showTooltip={{ label: $isMicEnabled ? love.string.Mute : love.string.UnMute }}
|
||||
showTooltip={{
|
||||
label: !$isMicAllowed ? love.string.MicPermission : $isMicEnabled ? love.string.Mute : love.string.UnMute
|
||||
}}
|
||||
disabled={!$isMicAllowed}
|
||||
action={changeMute}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={micSettings}
|
||||
@@ -208,8 +213,14 @@
|
||||
size={'large'}
|
||||
icon={$isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
showTooltip={{ label: $isCameraEnabled ? love.string.StopVideo : love.string.StartVideo }}
|
||||
disabled={!$isConnected}
|
||||
showTooltip={{
|
||||
label: !$isCamAllowed
|
||||
? love.string.CamPermission
|
||||
: $isCameraEnabled
|
||||
? love.string.StopVideo
|
||||
: love.string.StartVideo
|
||||
}}
|
||||
disabled={!$isCamAllowed}
|
||||
action={changeCam}
|
||||
secondIcon={IconUpOutline}
|
||||
secondAction={camSettings}
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
import { currentRoom, infos, myInfo, myOffice } from '../stores'
|
||||
import {
|
||||
awaitConnect,
|
||||
isCamAllowed,
|
||||
isCameraEnabled,
|
||||
isConnected,
|
||||
isMicAllowed,
|
||||
isMicEnabled,
|
||||
isShareWithSound,
|
||||
isSharingEnabled,
|
||||
@@ -316,17 +318,21 @@
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Mic : $isMicEnabled ? love.icon.MicEnabled : love.icon.MicDisabled}
|
||||
label={$isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
label={!$isMicAllowed ? love.string.MicPermission : $isMicEnabled ? love.string.Mute : love.string.UnMute}
|
||||
size={'small'}
|
||||
action={changeMute}
|
||||
disabled={!$isConnected}
|
||||
disabled={!$isConnected || !$isMicAllowed}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={!$isConnected ? love.icon.Cam : $isCameraEnabled ? love.icon.CamEnabled : love.icon.CamDisabled}
|
||||
label={$isCameraEnabled ? love.string.StopVideo : love.string.StartVideo}
|
||||
label={!$isCamAllowed
|
||||
? love.string.CamPermission
|
||||
: $isCameraEnabled
|
||||
? love.string.StopVideo
|
||||
: love.string.StartVideo}
|
||||
size={'small'}
|
||||
action={changeCam}
|
||||
disabled={!$isConnected || !allowCam}
|
||||
disabled={!$isConnected || !allowCam || !$isCamAllowed}
|
||||
/>
|
||||
{#if $isConnected}
|
||||
<ActionIcon
|
||||
|
||||
@@ -97,6 +97,8 @@ export default mergeIds(loveId, love, {
|
||||
MoreOptions: '' as IntlString,
|
||||
Language: '' as IntlString,
|
||||
WithAudio: '' as IntlString,
|
||||
ShareWithAudioTooltip: '' as IntlString
|
||||
ShareWithAudioTooltip: '' as IntlString,
|
||||
CamPermission: '' as IntlString,
|
||||
MicPermission: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -176,6 +176,8 @@ export const isCameraEnabled = writable<boolean>(false)
|
||||
export const isSharingEnabled = writable<boolean>(false)
|
||||
export const isFullScreen = writable<boolean>(false)
|
||||
export const isShareWithSound = writable<boolean>(false)
|
||||
export const isMicAllowed = writable<boolean>(false)
|
||||
export const isCamAllowed = writable<boolean>(false)
|
||||
|
||||
function handleTrackSubscribed (
|
||||
track: RemoteTrack,
|
||||
@@ -539,8 +541,9 @@ export async function setCam (value: boolean): Promise<void> {
|
||||
try {
|
||||
const opt: VideoCaptureOptions = {}
|
||||
const selectedDevice = localStorage.getItem(selectedCamId)
|
||||
const devices = await LKRoom.getLocalDevices('videoinput')
|
||||
isCamAllowed.set(devices.length > 0)
|
||||
if (selectedDevice !== null) {
|
||||
const devices = await LKRoom.getLocalDevices('videoinput')
|
||||
const available = devices.find((p) => p.deviceId === selectedDevice)
|
||||
if (available !== undefined) {
|
||||
opt.deviceId = available.deviceId
|
||||
@@ -549,6 +552,7 @@ export async function setCam (value: boolean): Promise<void> {
|
||||
await lk.localParticipant.setCameraEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
isCamAllowed.set(false)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_cam', value })
|
||||
@@ -572,8 +576,9 @@ export async function setMic (value: boolean): Promise<void> {
|
||||
try {
|
||||
const opt: AudioCaptureOptions = {}
|
||||
const selectedDevice = localStorage.getItem(selectedMicId)
|
||||
const devices = await LKRoom.getLocalDevices('audioinput')
|
||||
isMicAllowed.set(devices.length > 0)
|
||||
if (selectedDevice !== null) {
|
||||
const devices = await LKRoom.getLocalDevices('audioinput')
|
||||
const available = devices.find((p) => p.deviceId === selectedDevice)
|
||||
if (available !== undefined) {
|
||||
opt.deviceId = available.deviceId
|
||||
@@ -582,6 +587,7 @@ export async function setMic (value: boolean): Promise<void> {
|
||||
await lk.localParticipant.setMicrophoneEnabled(value, opt)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
isMicAllowed.set(false)
|
||||
}
|
||||
} else {
|
||||
sendMessage({ type: 'set_mic', value })
|
||||
|
||||
@@ -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,
|
||||
@@ -1094,11 +1094,11 @@ test.describe('QMS. Documents tests', () => {
|
||||
await documentContentPage.checkDocument(documentDetails)
|
||||
await documentContentPage.checkDocumentStatus(DocumentStatus.IN_REVIEW)
|
||||
|
||||
await expect(documentContentPage.contentLocator.locator('h1:first-child')).toHaveText(overview.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:first-child + p')).toHaveText(overview.content)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:nth-of-type(1)')).toHaveText(overview.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:nth-of-type(1) + p')).toHaveText(overview.content)
|
||||
|
||||
await expect(documentContentPage.contentLocator.locator('h1:not(:first-child)')).toHaveText(main.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:not(:first-child) + p')).toHaveText(main.content)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:nth-of-type(2)')).toHaveText(main.heading)
|
||||
await expect(documentContentPage.contentLocator.locator('h1:nth-of-type(2) + p')).toHaveText(main.content)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,15 +12,16 @@ export class DocumentApprovalsPage extends DocumentCommonPage {
|
||||
async checkRejectApproval (approvalName: string, message: string): Promise<void> {
|
||||
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<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,14 +733,14 @@ export class DocumentContentPage extends DocumentCommonPage {
|
||||
await this.confirmSubmission()
|
||||
}
|
||||
|
||||
async fillSelectApproversForm (approvers: Array<string>): Promise<void> {
|
||||
async fillSelectApproversForm (approvers: Array<string>, skipConfirm: boolean = false): Promise<void> {
|
||||
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<void> {
|
||||
|
||||
Reference in New Issue
Block a user