mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-22 09:35:00 +02:00
UBERF-7090: Add QMS plugins (#5716)
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
+204
@@ -0,0 +1,204 @@
|
||||
<!--
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Heading, isEmptyMarkup } from '@hcengineering/text-editor'
|
||||
import { Button, Component, EditBox } from '@hcengineering/ui'
|
||||
import documents, { Document, DocumentSection, DocumentTemplateSection } from '@hcengineering/controlled-documents'
|
||||
|
||||
import Info from '../icons/Info.svelte'
|
||||
import {
|
||||
$documentCommentHighlightedLocation as highlighted,
|
||||
$canAddDocumentComments as canAddDocumentComments,
|
||||
$canViewDocumentComments as canViewDocumentComments,
|
||||
$collapsedDocumentSectionIds as collapsedSectionIds,
|
||||
$documentSectionEditingDescription as documentSectionEditingDescription,
|
||||
$groupedDocumentComments as groupedDocumentComments,
|
||||
$isEditable as isEditable,
|
||||
documentCommentsDisplayRequested,
|
||||
documentSectionToggled,
|
||||
showAddCommentPopupFx,
|
||||
updateDocumentSectionDescriptionFx
|
||||
} from '../../stores/editors/document'
|
||||
import { openGuidanceEditor } from '../../utils'
|
||||
import FieldSectionEditor from '../FieldSectionEditor.svelte'
|
||||
import DescriptionEditor from './editors/DescriptionEditor.svelte'
|
||||
|
||||
export let document: Document
|
||||
export let value: DocumentSection
|
||||
export let index: number
|
||||
export let dragging = false
|
||||
|
||||
export let headings: Heading[] = [] // out
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const h = client.getHierarchy()
|
||||
|
||||
$: editor = h.as(h.getClass(value._class), documents.mixin.DocumentSectionEditor)?.editor
|
||||
|
||||
let sectionHasGuidance = false
|
||||
let templateSection: DocumentTemplateSection | undefined
|
||||
$: getDocumentTemplateSection(value)
|
||||
async function getDocumentTemplateSection (section: DocumentSection) {
|
||||
if (h.hasMixin(value, documents.mixin.DocumentTemplateSection)) {
|
||||
templateSection = h.as(value, documents.mixin.DocumentTemplateSection)
|
||||
} else if (value.templateSectionId != null) {
|
||||
templateSection = await client.findOne(documents.mixin.DocumentTemplateSection, { _id: value.templateSectionId })
|
||||
}
|
||||
|
||||
if (templateSection) {
|
||||
const guidance = templateSection.guidance
|
||||
sectionHasGuidance = !isEmptyMarkup(guidance)
|
||||
}
|
||||
}
|
||||
|
||||
let contentHeadings: Heading[] = []
|
||||
$: sectionHeading = makeSectionHeading(value, index)
|
||||
$: headings = [sectionHeading, ...contentHeadings]
|
||||
|
||||
function handleSectionContentHeadings (headings: Heading[]): void {
|
||||
contentHeadings = headings
|
||||
}
|
||||
|
||||
function makeSectionHeading (section: DocumentSection, index: number): Heading {
|
||||
index = index + 1
|
||||
const id = `section-${index}`
|
||||
const title = `${index}. ${section.title}`
|
||||
return { id, title, level: 0 }
|
||||
}
|
||||
|
||||
async function handleOpenAddCommentPopup (ev?: Event): Promise<void> {
|
||||
if (!$canAddDocumentComments) {
|
||||
return
|
||||
}
|
||||
|
||||
await showAddCommentPopupFx({
|
||||
element: (ev as MouseEvent).target as HTMLElement,
|
||||
sectionKey: value.key
|
||||
})
|
||||
}
|
||||
|
||||
function handleDisplayDocumentComments (ev?: Event): void {
|
||||
if (!$canViewDocumentComments) {
|
||||
return
|
||||
}
|
||||
if (!$groupedDocumentComments.hasDocumentComments(value.key)) {
|
||||
return
|
||||
}
|
||||
|
||||
ev?.stopPropagation()
|
||||
|
||||
documentCommentsDisplayRequested({ element: (ev as MouseEvent).target as HTMLElement, sectionKey: value.key })
|
||||
}
|
||||
|
||||
async function updateSectionTitle (title: string) {
|
||||
await client.update(value, { title })
|
||||
}
|
||||
|
||||
let sectionElement: HTMLElement
|
||||
export function getSectionElement (): HTMLElement {
|
||||
return sectionElement
|
||||
}
|
||||
|
||||
let title = value.title
|
||||
|
||||
let descr = ''
|
||||
$: getDescription(templateSection)
|
||||
$: isActiveSectionNode = !!$highlighted && $highlighted.sectionKey === value.key && !$highlighted.nodeId
|
||||
function getDescription (section?: DocumentTemplateSection) {
|
||||
if (section != null) {
|
||||
descr = h.as(section, documents.mixin.DocumentTemplateSection).description ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
$: isEditingDescription = $documentSectionEditingDescription === value._id
|
||||
|
||||
async function stopEditingDescription () {
|
||||
if (!templateSection) {
|
||||
return
|
||||
}
|
||||
updateDocumentSectionDescriptionFx({ section: templateSection, description: descr })
|
||||
}
|
||||
|
||||
function showGuidance (ev: MouseEvent) {
|
||||
if (templateSection == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const isEditingTemplate = templateSection._id === value._id
|
||||
openGuidanceEditor(client, templateSection, index + 1, isEditingTemplate ? 'canEdit' : 'readonly', ev)
|
||||
}
|
||||
</script>
|
||||
|
||||
<FieldSectionEditor
|
||||
editable={$isEditable}
|
||||
expanded={!$collapsedSectionIds.has(value._id)}
|
||||
on:toggle={() => documentSectionToggled(value._id)}
|
||||
>
|
||||
<svelte:fragment slot="before-header">
|
||||
<slot name="before-header" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="index">
|
||||
{index + 1}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id={sectionHeading.id}
|
||||
class:text-editor-highlighted-node-warning={isActiveSectionNode ||
|
||||
$groupedDocumentComments.hasDocumentComments(value.key)}
|
||||
class:text-editor-highlighted-node-selected={isActiveSectionNode}
|
||||
bind:this={sectionElement}
|
||||
on:click={handleDisplayDocumentComments}
|
||||
>
|
||||
{#if $isEditable && !dragging}
|
||||
<EditBox maxWidth="100%" bind:value={title} propagateClick on:change={() => updateSectionTitle(title)} />
|
||||
{:else}
|
||||
{title}
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header-extra">
|
||||
<div class="flex-row-center ml-2 mr-10">
|
||||
{#if $canAddDocumentComments}
|
||||
<Button icon={chunter.icon.Chunter} kind="list-header" size="small" on:click={handleOpenAddCommentPopup} />
|
||||
{/if}
|
||||
{#if templateSection && sectionHasGuidance}
|
||||
<Button icon={Info} kind="list-header" size="small" on:click={showGuidance} />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="before-content">
|
||||
{#if isEditingDescription || descr.length > 0}
|
||||
<div class="section-descr no-print">
|
||||
<DescriptionEditor bind:value={descr} disabled={!isEditingDescription} on:blur={stopEditingDescription} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<Component
|
||||
is={editor}
|
||||
props={{
|
||||
value,
|
||||
document,
|
||||
onHeadings: handleSectionContentHeadings
|
||||
}}
|
||||
on:change
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</FieldSectionEditor>
|
||||
@@ -0,0 +1,197 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { Account, TypedSpace, type Data, type Ref, getCurrentAccount, type Permission } from '@hcengineering/core'
|
||||
import contact, { PersonAccount, type Employee } from '@hcengineering/contact'
|
||||
import documents, { type ControlledDocument } from '@hcengineering/controlled-documents'
|
||||
import { UserBoxItems } from '@hcengineering/contact-resources'
|
||||
import { type PermissionsStore, permissionsStore } from '@hcengineering/view-resources'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
|
||||
export let controlledDoc: Data<ControlledDocument>
|
||||
export let space: Ref<TypedSpace>
|
||||
export let canChangeReviewers: boolean = true
|
||||
export let canChangeApprovers: boolean = true
|
||||
export let canChangeCoAuthors: boolean = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const currentAccount = getCurrentAccount()
|
||||
|
||||
$: reviewers = controlledDoc.reviewers
|
||||
$: approvers = controlledDoc.approvers
|
||||
$: coAuthors = controlledDoc.coAuthors
|
||||
|
||||
$: permissionsSpace = space === documents.space.UnsortedTemplates ? documents.space.QualityDocuments : space
|
||||
|
||||
function getPermittedAccounts (
|
||||
permission: Ref<Permission>,
|
||||
space: Ref<TypedSpace>,
|
||||
permissionsStore: PermissionsStore
|
||||
): Array<Ref<Account>> {
|
||||
return Array.from(permissionsStore.ap[space]?.[permission] ?? [])
|
||||
}
|
||||
|
||||
let permittedReviewers: Array<Ref<Employee>> = []
|
||||
$: permittedReviewerAccounts = getPermittedAccounts(
|
||||
documents.permission.ReviewDocument,
|
||||
permissionsSpace,
|
||||
$permissionsStore
|
||||
)
|
||||
const prQuery = createQuery()
|
||||
$: if (permittedReviewerAccounts.length > 0) {
|
||||
prQuery.query(
|
||||
contact.class.PersonAccount,
|
||||
{
|
||||
_id: { $in: Array.from(permittedReviewerAccounts) as Array<Ref<PersonAccount>> }
|
||||
},
|
||||
(res) => {
|
||||
permittedReviewers = res.map((pa) => pa.person) as Array<Ref<Employee>>
|
||||
}
|
||||
)
|
||||
} else {
|
||||
permittedReviewers = []
|
||||
}
|
||||
|
||||
let permittedApprovers: Array<Ref<Employee>> = []
|
||||
$: permittedApproverAccounts = getPermittedAccounts(
|
||||
documents.permission.ApproveDocument,
|
||||
permissionsSpace,
|
||||
$permissionsStore
|
||||
)
|
||||
const paQuery = createQuery()
|
||||
$: if (permittedApproverAccounts.length > 0) {
|
||||
paQuery.query(
|
||||
contact.class.PersonAccount,
|
||||
{
|
||||
_id: { $in: Array.from(permittedApproverAccounts) as Array<Ref<PersonAccount>> }
|
||||
},
|
||||
(res) => {
|
||||
permittedApprovers = res.map((pa) => pa.person) as Array<Ref<Employee>>
|
||||
}
|
||||
)
|
||||
} else {
|
||||
permittedApprovers = []
|
||||
}
|
||||
|
||||
let permittedCoAuthors: Array<Ref<Employee>> = []
|
||||
$: permittedCoAuthorAccounts = getPermittedAccounts(
|
||||
documents.permission.CoAuthorDocument,
|
||||
permissionsSpace,
|
||||
$permissionsStore
|
||||
).filter((acc) => acc !== currentAccount._id)
|
||||
const pcaQuery = createQuery()
|
||||
$: if (permittedCoAuthorAccounts.length > 0) {
|
||||
pcaQuery.query(
|
||||
contact.class.PersonAccount,
|
||||
{
|
||||
_id: { $in: Array.from(permittedCoAuthorAccounts) as Array<Ref<PersonAccount>> }
|
||||
},
|
||||
(res) => {
|
||||
permittedCoAuthors = res.map((pa) => pa.person) as Array<Ref<Employee>>
|
||||
}
|
||||
)
|
||||
} else {
|
||||
permittedCoAuthors = []
|
||||
}
|
||||
|
||||
function handleUsersUpdated (type: 'reviewers' | 'approvers' | 'coAuthors', users: Ref<Employee>[]): void {
|
||||
dispatch('update', { type, users })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col">
|
||||
<div class="flex labelContainer">
|
||||
<div class="label mr-1">
|
||||
<Label label={documents.string.CoAuthors} />
|
||||
</div>
|
||||
{reviewers?.length}
|
||||
</div>
|
||||
<div class="flex-col mt-4">
|
||||
<UserBoxItems
|
||||
items={coAuthors}
|
||||
docQuery={{
|
||||
active: true,
|
||||
_id: { $in: permittedCoAuthors }
|
||||
}}
|
||||
label={documents.string.CoAuthors}
|
||||
readonly={!canChangeCoAuthors}
|
||||
on:update={({ detail }) => {
|
||||
handleUsersUpdated('coAuthors', detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 mb-6 divider" />
|
||||
<div class="flex labelContainer">
|
||||
<div class="label mr-1">
|
||||
<Label label={documents.string.Reviewers} />
|
||||
</div>
|
||||
{reviewers?.length}
|
||||
</div>
|
||||
<div class="flex-col mt-4">
|
||||
<UserBoxItems
|
||||
items={reviewers}
|
||||
docQuery={{
|
||||
active: true,
|
||||
_id: { $in: permittedReviewers }
|
||||
}}
|
||||
label={documents.string.Reviewers}
|
||||
readonly={!canChangeReviewers}
|
||||
on:update={({ detail }) => {
|
||||
handleUsersUpdated('reviewers', detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 mb-6 divider" />
|
||||
<div class="flex labelContainer">
|
||||
<div class="label mr-1">
|
||||
<Label label={documents.string.Approvers} />
|
||||
</div>
|
||||
{approvers?.length}
|
||||
</div>
|
||||
<div class="flex-col mt-4">
|
||||
<UserBoxItems
|
||||
items={approvers}
|
||||
docQuery={{
|
||||
active: true,
|
||||
_id: { $in: permittedApprovers }
|
||||
}}
|
||||
label={documents.string.Approvers}
|
||||
readonly={!canChangeApprovers}
|
||||
on:update={({ detail }) => {
|
||||
handleUsersUpdated('approvers', detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.labelContainer {
|
||||
min-width: 11rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--theme-qms-form-row-label-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
min-height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
}
|
||||
</style>
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from 'svelte'
|
||||
import { Doc as Ydoc } from 'yjs'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type Doc } from '@hcengineering/core'
|
||||
import {
|
||||
CollaborationIds,
|
||||
StringDiffViewer,
|
||||
TiptapCollabProvider,
|
||||
createTiptapCollaborationData,
|
||||
formatCollaborativeDocumentId
|
||||
} from '@hcengineering/text-editor'
|
||||
import { Dropdown, Label, ListItem, Loading, Scroller, themeStore } from '@hcengineering/ui'
|
||||
import documents, {
|
||||
ControlledDocument,
|
||||
ControlledDocumentSnapshot,
|
||||
ControlledDocumentState,
|
||||
Document,
|
||||
DocumentSection,
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import plugin from '../../plugin'
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
$controlledDocumentSections as sections,
|
||||
$comparedDocument as compareTo,
|
||||
$comparedDocumentSections as compareToSections,
|
||||
$documentComparisonVersions as documentComparisonVersions,
|
||||
ComparisonSectionPair,
|
||||
comparisonRequested,
|
||||
loadComparedDocumentSectionsFx
|
||||
} from '../../stores/editors/document'
|
||||
import { COLLABORATOR_URL, TOKEN, getTranslatedControlledDocStates, getTranslatedDocumentStates } from '../../utils'
|
||||
import DocumentSectionPairDiffViewer from './DocumentSectionPairDiffViewer.svelte'
|
||||
import DocumentTitle from './DocumentTitle.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const ydoc = getContext<Ydoc>(CollaborationIds.Doc)
|
||||
|
||||
let collapsedPairIndices = new Set<number>()
|
||||
let comparedYdoc: Ydoc | undefined = undefined
|
||||
let comparedProvider: TiptapCollabProvider | undefined = undefined
|
||||
let loading = true
|
||||
const isLoadPending = loadComparedDocumentSectionsFx.pending
|
||||
|
||||
const handleSectionDiffPairs = (firstSections: DocumentSection[], secondSections: DocumentSection[]) => {
|
||||
const result: ComparisonSectionPair[] = []
|
||||
const firstSectionKeys = new Set(firstSections.map((section) => section.key))
|
||||
const secondSectionKeys = new Set(secondSections.map((section) => section.key))
|
||||
let secondIndex = 0
|
||||
let firstIndex = 0
|
||||
while (firstIndex < firstSections.length) {
|
||||
const firstSection = firstSections[firstIndex]
|
||||
if (secondSectionKeys.has(firstSection.key)) {
|
||||
while (secondIndex < secondSections.length && !firstSectionKeys.has(secondSections[secondIndex].key)) {
|
||||
result.push([null, { section: secondSections[secondIndex], index: secondIndex + 1 }])
|
||||
secondIndex++
|
||||
}
|
||||
}
|
||||
if (secondIndex < secondSections.length && firstSection.key === secondSections[secondIndex].key) {
|
||||
result.push([
|
||||
{ section: firstSection, index: firstIndex + 1 },
|
||||
{ section: secondSections[secondIndex], index: secondIndex + 1 }
|
||||
])
|
||||
secondIndex++
|
||||
} else {
|
||||
result.push([{ section: firstSection, index: firstIndex + 1 }, null])
|
||||
}
|
||||
firstIndex++
|
||||
}
|
||||
|
||||
while (secondIndex < secondSections.length) {
|
||||
result.push([null, { section: secondSections[secondIndex], index: secondIndex + 1 }])
|
||||
secondIndex++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const handleSelect = (event: CustomEvent<ListItem>) => {
|
||||
const version = $documentComparisonVersions.find((item) => item._id === event.detail._id)
|
||||
if (version) {
|
||||
comparisonRequested(version)
|
||||
}
|
||||
}
|
||||
|
||||
let translatedStates: Readonly<Record<DocumentState | ControlledDocumentState, string>> | null = null
|
||||
function getTranslatedLabels (lang: string) {
|
||||
Promise.all([getTranslatedDocumentStates(lang), getTranslatedControlledDocStates(lang)]).then(
|
||||
([states, controlledStates]) => {
|
||||
translatedStates = {
|
||||
...states,
|
||||
...controlledStates
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function isDocument (document: Doc | null): document is Document {
|
||||
if (document == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hierarchy.isDerived(document._class, documents.class.Document)
|
||||
}
|
||||
|
||||
const generateVersionName = (
|
||||
document: ControlledDocument | ControlledDocumentSnapshot,
|
||||
translatedStates: Readonly<Record<DocumentState | ControlledDocumentState, string>> | null
|
||||
): string => {
|
||||
let state: ControlledDocumentState | DocumentState | undefined = document.controlledState
|
||||
if (state == null) {
|
||||
state = document.state ?? DocumentState.Draft
|
||||
}
|
||||
|
||||
if (isDocument(document)) {
|
||||
return `v${document.major}.${document.minor} | ${translatedStates ? translatedStates[state] : ''}`
|
||||
} else {
|
||||
return `${document.name} | ${translatedStates ? translatedStates[state] : ''}`
|
||||
}
|
||||
}
|
||||
|
||||
$: getTranslatedLabels($themeStore.language)
|
||||
$: versionItems = $documentComparisonVersions.map((version) => ({
|
||||
_id: version._id,
|
||||
label: generateVersionName(version, translatedStates)
|
||||
}))
|
||||
$: if ($compareTo) {
|
||||
if (comparedProvider) {
|
||||
comparedProvider.disconnect()
|
||||
}
|
||||
loading = true
|
||||
const collaborativeDoc = $compareTo.content
|
||||
|
||||
const data = createTiptapCollaborationData({
|
||||
collaboratorURL: COLLABORATOR_URL,
|
||||
token: TOKEN,
|
||||
documentId: formatCollaborativeDocumentId(collaborativeDoc)
|
||||
})
|
||||
comparedYdoc = data.ydoc
|
||||
comparedProvider = data.provider
|
||||
comparedProvider.loaded.then(() => (loading = false))
|
||||
}
|
||||
|
||||
$: sectionDiffPairs = handleSectionDiffPairs($sections, $compareToSections)
|
||||
|
||||
onDestroy(() => {
|
||||
comparedProvider?.destroy()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-gap-2 h-12 pl-7 items-center bottom-divider">
|
||||
<Label label={plugin.string.Compare} />
|
||||
<Dropdown
|
||||
items={versionItems}
|
||||
disabled
|
||||
selected={versionItems.find((item) => item._id === $controlledDocument?._id)}
|
||||
withSearch={false}
|
||||
placeholder={documents.string.Version}
|
||||
/>
|
||||
<Label label={plugin.string.Against} />
|
||||
<Dropdown
|
||||
items={versionItems}
|
||||
selected={versionItems.find((item) => item._id === $compareTo?._id)}
|
||||
withSearch={false}
|
||||
placeholder={documents.string.Version}
|
||||
on:selected={handleSelect}
|
||||
/>
|
||||
</div>
|
||||
{#if loading || $isLoadPending}
|
||||
<Loading />
|
||||
{:else}
|
||||
<Scroller>
|
||||
<div class="antiAccordion">
|
||||
<div class="pl-7">
|
||||
<DocumentTitle>
|
||||
<StringDiffViewer
|
||||
value={$controlledDocument?.title ?? ''}
|
||||
compareTo={(isDocument($compareTo) ? $compareTo : $controlledDocument)?.title ?? ''}
|
||||
/>
|
||||
</DocumentTitle>
|
||||
</div>
|
||||
{#each sectionDiffPairs as pair, index}
|
||||
<DocumentSectionPairDiffViewer
|
||||
{pair}
|
||||
firstYdoc={ydoc}
|
||||
secondYdoc={comparedYdoc}
|
||||
expanded={!collapsedPairIndices.has(index)}
|
||||
on:toggle={() => {
|
||||
if (collapsedPairIndices.has(index)) {
|
||||
collapsedPairIndices.delete(index)
|
||||
} else {
|
||||
collapsedPairIndices.add(index)
|
||||
}
|
||||
collapsedPairIndices = new Set(collapsedPairIndices)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { Label, Scroller } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import documents, { type ChangeControl } from '@hcengineering/controlled-documents'
|
||||
|
||||
import documentsRes from '../../plugin'
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
$documentReleasedVersions as documentReleasedVersions
|
||||
} from '../../stores/editors/document/editor'
|
||||
import { documentCompareFn, getDocumentVersionString } from '../../utils'
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let changeControls: Record<Ref<ChangeControl>, ChangeControl> = {}
|
||||
$: if ($documentReleasedVersions.length > 0) {
|
||||
void client
|
||||
.findAll(documents.class.ChangeControl, {
|
||||
_id: { $in: $documentReleasedVersions.map((v) => v.changeControl) }
|
||||
})
|
||||
.then((res) => {
|
||||
changeControls = res.reduce<typeof changeControls>((prev, curr) => {
|
||||
prev[curr._id] = curr
|
||||
return prev
|
||||
}, {})
|
||||
})
|
||||
}
|
||||
|
||||
$: orderedVersions = $documentReleasedVersions
|
||||
.filter((doc) => {
|
||||
if ($controlledDocument == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
doc.major < $controlledDocument.major ||
|
||||
(doc.major === $controlledDocument.major && doc.minor <= $controlledDocument.minor)
|
||||
)
|
||||
})
|
||||
.toSorted(documentCompareFn)
|
||||
|
||||
function getDescription (cc: ChangeControl | undefined): string {
|
||||
if (cc === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return [cc.reason, cc.description].filter((s) => s !== '' && s !== undefined).join('\n')
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
<div class="root">
|
||||
{#if orderedVersions.length > 0}
|
||||
<div class="flex-col list">
|
||||
{#each orderedVersions as version}
|
||||
<div class="row flex-row-top px-4">
|
||||
<div class="flex-col col">
|
||||
<div class="fs-title text-normal version">
|
||||
{getDocumentVersionString(version)}
|
||||
</div>
|
||||
{#if version.effectiveDate !== undefined}
|
||||
<div class="date">
|
||||
{new Date(version.effectiveDate).toLocaleDateString('default', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="description">
|
||||
{getDescription(changeControls[version.changeControl])}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Label label={documentsRes.string.FirstDraftVersion} />
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: 1.5rem 3.25rem;
|
||||
|
||||
@media print {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 0 0 6rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
gap: 3rem;
|
||||
@media print {
|
||||
border-left: 2px solid var(--theme-divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--theme-dark-color);
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import DocumentHistory from './DocumentHistory.svelte'
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<DocumentHistory />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: 1.5rem 3.25rem;
|
||||
}
|
||||
</style>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { Doc as Ydoc } from 'yjs'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { CollaborationDiffViewer, StringDiffViewer } from '@hcengineering/text-editor'
|
||||
import documents, { DocumentSection, CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import plugin from '../../plugin'
|
||||
import { ComparisonSectionPair } from '../../stores/editors/document'
|
||||
import FieldSectionEditor from '../FieldSectionEditor.svelte'
|
||||
|
||||
export let pair: ComparisonSectionPair
|
||||
export let firstYdoc: Ydoc
|
||||
export let secondYdoc: Ydoc | undefined = undefined
|
||||
export let expanded = false
|
||||
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
|
||||
const getSectionField = (section: DocumentSection | undefined | null) => {
|
||||
if (!section) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return (section as CollaborativeDocumentSection).collaboratorSectionId
|
||||
}
|
||||
$: _class = pair[0]?.section._class ?? pair[1]?.section._class
|
||||
</script>
|
||||
|
||||
<FieldSectionEditor {expanded} editable={false} on:toggle>
|
||||
<svelte:fragment slot="index">
|
||||
<StringDiffViewer value={`${pair[0]?.index ?? ''}`} compareTo={`${pair[1]?.index ?? ''}`} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="header">
|
||||
<StringDiffViewer value={pair[0]?.section.title ?? ''} compareTo={pair[1]?.section.title ?? ''} />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
{#if _class && hierarchy.isDerived(_class, documents.class.CollaborativeDocumentSection)}
|
||||
<CollaborationDiffViewer
|
||||
field={getSectionField(pair[0]?.section)}
|
||||
comparedField={getSectionField(pair[1]?.section)}
|
||||
ydoc={firstYdoc}
|
||||
comparedYdoc={secondYdoc}
|
||||
/>
|
||||
{:else}
|
||||
{plugin.string.ComparisonModeNotSupported}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</FieldSectionEditor>
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Label, Scroller, getUserTimezone } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import documents, { DocumentApprovalRequest, DocumentReviewRequest } from '@hcengineering/controlled-documents'
|
||||
import { employeeByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Employee, Person, formatName } from '@hcengineering/contact'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
|
||||
import documentsRes from '../../plugin'
|
||||
import { $controlledDocument as controlledDocument } from '../../stores/editors/document/editor'
|
||||
|
||||
interface Signer {
|
||||
id?: Ref<Person>
|
||||
role: 'author' | 'reviewer' | 'approver'
|
||||
name: string
|
||||
date: string
|
||||
}
|
||||
|
||||
let signers: Signer[] = []
|
||||
|
||||
let reviewRequest: DocumentReviewRequest
|
||||
let approvalRequest: DocumentApprovalRequest
|
||||
|
||||
const reviewQuery = createQuery()
|
||||
const approvalQuery = createQuery()
|
||||
const timeZone: string = getUserTimezone()
|
||||
|
||||
$: if ($controlledDocument !== undefined) {
|
||||
reviewQuery.query(
|
||||
documents.class.DocumentReviewRequest,
|
||||
{
|
||||
attachedTo: $controlledDocument?._id,
|
||||
attachedToClass: $controlledDocument?._class
|
||||
},
|
||||
(res) => {
|
||||
reviewRequest = res[0]
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending },
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
$: 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
|
||||
|
||||
return rawName !== undefined ? formatName(rawName) : ''
|
||||
}
|
||||
|
||||
signers = [
|
||||
{
|
||||
id: $controlledDocument.author,
|
||||
role: 'author',
|
||||
name: getNameByEmployeeId($controlledDocument.author),
|
||||
date: $controlledDocument.createdOn !== undefined ? formatDate($controlledDocument.createdOn) : ''
|
||||
}
|
||||
]
|
||||
|
||||
if (reviewRequest !== undefined) {
|
||||
reviewRequest.approved.forEach((reviewer, idx) => {
|
||||
const rAcc = $personAccountByIdStore.get(reviewer)
|
||||
const date = reviewRequest.approvedDates?.[idx]
|
||||
|
||||
signers.push({
|
||||
id: rAcc?.person,
|
||||
role: 'reviewer',
|
||||
name: getNameByEmployeeId(rAcc?.person),
|
||||
date: formatDate(date ?? reviewRequest.modifiedOn)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (approvalRequest !== undefined) {
|
||||
approvalRequest.approved.forEach((approver, idx) => {
|
||||
const aAcc = $personAccountByIdStore.get(approver)
|
||||
const date = approvalRequest.approvedDates?.[idx]
|
||||
|
||||
signers.push({
|
||||
id: aAcc?.person,
|
||||
role: 'approver',
|
||||
name: getNameByEmployeeId(aAcc?.person),
|
||||
date: formatDate(date ?? approvalRequest.modifiedOn)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate (date: number): string {
|
||||
return new Date(date).toLocaleDateString('default', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
timeZone,
|
||||
timeZoneName: 'short',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
function getSignerLabel (role: 'author' | 'reviewer' | 'approver'): IntlString {
|
||||
switch (role) {
|
||||
case 'author':
|
||||
return documentsRes.string.Author
|
||||
case 'reviewer':
|
||||
return documentsRes.string.Reviewer
|
||||
case 'approver':
|
||||
return documentsRes.string.Approver
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
<div class="root">
|
||||
<div class="flex-col list">
|
||||
{#each signers as signer}
|
||||
<div class="row flex-row-top px-4">
|
||||
<div class="flex-col col">
|
||||
<div class="fs-title text-normal version">
|
||||
<Label label={getSignerLabel(signer.role)} />
|
||||
</div>
|
||||
|
||||
<div class="date">
|
||||
{signer.date}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="name">
|
||||
{signer.name}
|
||||
</div>
|
||||
<div class="code">
|
||||
{signer.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.list {
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex: 0 0 6rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
gap: 3rem;
|
||||
@media print {
|
||||
border-left: 2px solid var(--theme-divider-color);
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--theme-dark-color);
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="title font-semi-bold pt-6 pb-6">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
font-size: 2.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
</style>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright @ 2022 Hardcore Engineering Inc
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Document } from '@hcengineering/controlled-documents'
|
||||
import { Class, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewletPreference, ViewOptions } from '@hcengineering/view'
|
||||
|
||||
export let _class: Ref<Class<Document>>
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let viewOptions: ViewOptions
|
||||
export let query: DocumentQuery<Document> = {}
|
||||
export let space: Ref<Space> | undefined
|
||||
export let preference: ViewletPreference | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if viewlet?.$lookup?.descriptor?.component}
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
_class,
|
||||
config: preference?.config || viewlet.config,
|
||||
options: viewlet.options,
|
||||
viewlet,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
space,
|
||||
query,
|
||||
enableChecking: true
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@@ -0,0 +1,274 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Heading, TableOfContents, TableOfContentsContent } from '@hcengineering/text-editor'
|
||||
import { EditBox, IconCircles, Scroller } from '@hcengineering/ui'
|
||||
import { showMenu } from '@hcengineering/view-resources'
|
||||
import { DocumentSection, calcRank } from '@hcengineering/controlled-documents'
|
||||
import { onDestroy, tick } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
documentCommentsLocationNavigateRequested,
|
||||
documentSectionCollapsed,
|
||||
documentSectionExpanded,
|
||||
$isEditable as isEditable,
|
||||
$controlledDocumentSectionIds as sectionIds,
|
||||
$controlledDocumentSections as sections
|
||||
} from '../../stores/editors/document'
|
||||
import DocSectionEditor from './DocSectionEditor.svelte'
|
||||
import DocumentTitle from './DocumentTitle.svelte'
|
||||
import DocumentPrintTitlePage from '../print/DocumentPrintTitlePage.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const sectionRefs: Record<string, HTMLElement> = {}
|
||||
let divScroll: HTMLElement
|
||||
const editors: DocSectionEditor[] = []
|
||||
const sectionHeadings: Record<Ref<DocumentSection>, Heading[]> = {}
|
||||
|
||||
let draggingId: Ref<DocumentSection> | null = null
|
||||
let dragOverId: Ref<DocumentSection> | null = null
|
||||
let dragging = false
|
||||
|
||||
let title = $controlledDocument?.title ?? ''
|
||||
|
||||
$: headings = $sectionIds.map((sectionId) => sectionHeadings[sectionId] ?? []).flat()
|
||||
|
||||
const unsubscribeNavigateToLocation = documentCommentsLocationNavigateRequested.subscribe({
|
||||
next: ({ sectionKey }) => {
|
||||
const element = sectionRefs[sectionKey]
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleUpdateTitle = () => {
|
||||
if (!$controlledDocument || !title) {
|
||||
return
|
||||
}
|
||||
const titleTrimmed = title.trim()
|
||||
|
||||
if (titleTrimmed.length > 0 && titleTrimmed !== $controlledDocument.title) {
|
||||
client.update($controlledDocument, { title: titleTrimmed })
|
||||
}
|
||||
}
|
||||
|
||||
function resetDrag (ev?: DragEvent): void {
|
||||
draggingId = null
|
||||
dragOverId = null
|
||||
dragging = false
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, id: Ref<DocumentSection>): void {
|
||||
if (ev.dataTransfer) {
|
||||
dragging = true
|
||||
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
const index = $sectionIds.indexOf(id)
|
||||
ev.dataTransfer.setDragImage(editors[index].getSectionElement(), 0, 0)
|
||||
|
||||
draggingId = id
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDrop (ev: DragEvent, toId: Ref<DocumentSection>): Promise<void> {
|
||||
if (ev.dataTransfer && draggingId !== null && toId !== draggingId) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
const draggingIndex = $sectionIds.indexOf(draggingId)
|
||||
const toIndex = $sectionIds.indexOf(toId)
|
||||
|
||||
const [prev, next] = [
|
||||
$sections[draggingIndex < toIndex ? toIndex : toIndex - 1],
|
||||
$sections[draggingIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
|
||||
const section = $sections[draggingIndex]
|
||||
|
||||
// workaround to not display editor toolbar
|
||||
documentSectionCollapsed(draggingId)
|
||||
documentSectionCollapsed(toId)
|
||||
|
||||
await client.update(section, { rank: calcRank(prev, next) })
|
||||
}
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function openSectionMenu (ev: MouseEvent, section: DocumentSection): void {
|
||||
if (!$sections || $sections.length === 0) {
|
||||
return
|
||||
}
|
||||
showMenu(ev, { object: section })
|
||||
}
|
||||
|
||||
async function handleShowHeading (heading: Heading): Promise<void> {
|
||||
const sectionId = $sectionIds.find((sectionId) => sectionHeadings[sectionId]?.includes(heading))
|
||||
if (sectionId) {
|
||||
documentSectionExpanded(sectionId)
|
||||
}
|
||||
|
||||
await tick()
|
||||
|
||||
const element = window.document.getElementById(heading.id)
|
||||
element?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
onDestroy(unsubscribeNavigateToLocation)
|
||||
</script>
|
||||
|
||||
{#if $controlledDocument}
|
||||
<DocumentPrintTitlePage />
|
||||
|
||||
<div class="root flex-col relative">
|
||||
<div class="toc">
|
||||
<TableOfContents items={headings} on:select={(ev) => handleShowHeading(ev.detail)} />
|
||||
</div>
|
||||
{#if headings.length > 0}
|
||||
<div class="only-print ml-12">
|
||||
<TableOfContentsContent items={headings} />
|
||||
</div>
|
||||
<div class="pagebreak" />
|
||||
{/if}
|
||||
<Scroller bind:divScroll>
|
||||
<div class="antiAccordion">
|
||||
<div class="doc-title">
|
||||
<DocumentTitle>
|
||||
{#if $isEditable}
|
||||
<EditBox
|
||||
value={title}
|
||||
on:value={(event) => {
|
||||
title = event.detail
|
||||
}}
|
||||
on:blur={handleUpdateTitle}
|
||||
/>
|
||||
{:else}
|
||||
{$controlledDocument.title}
|
||||
{/if}
|
||||
</DocumentTitle>
|
||||
</div>
|
||||
{#if $sections}
|
||||
{#each $sections as section, i (section._id)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="row"
|
||||
bind:this={sectionRefs[section.key]}
|
||||
class:is-dragging={section._id === draggingId}
|
||||
class:drag-over-highlight={section._id === dragOverId}
|
||||
on:dragstart={(ev) => {
|
||||
handleDragStart(ev, section._id)
|
||||
}}
|
||||
on:dragleave|preventDefault={() => {
|
||||
if (dragOverId === section._id) dragOverId = null
|
||||
return false
|
||||
}}
|
||||
on:dragover|preventDefault={() => {
|
||||
dragOverId = section._id
|
||||
return false
|
||||
}}
|
||||
on:dragend={resetDrag}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, section._id)}
|
||||
animate:flip={{ duration: 400 }}
|
||||
>
|
||||
<DocSectionEditor
|
||||
bind:headings={sectionHeadings[section._id]}
|
||||
bind:this={editors[i]}
|
||||
value={section}
|
||||
document={$controlledDocument}
|
||||
index={i}
|
||||
{dragging}
|
||||
on:change={resetDrag}
|
||||
>
|
||||
<div class="m0 flex-row-center draggable-container" slot="before-header">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="draggable-mark"
|
||||
class:dragging
|
||||
on:click={(ev) => {
|
||||
openSectionMenu(ev, section)
|
||||
}}
|
||||
>
|
||||
<IconCircles size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</DocSectionEditor>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
@media print {
|
||||
margin-left: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.toc {
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
pointer-events: all;
|
||||
left: 1px;
|
||||
top: 1rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
padding-left: 3.25rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
position: relative;
|
||||
margin-left: 0;
|
||||
|
||||
.draggable-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.draggable-mark {
|
||||
padding: 0.375rem 0.125rem;
|
||||
&:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
position: relative;
|
||||
align-self: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-over-highlight {
|
||||
opacity: 0.2;
|
||||
}
|
||||
</style>
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { Label, Scroller, PlainTextEditor } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import documents, { type DocumentSpace, type ChangeControl, DocumentState } from '@hcengineering/controlled-documents'
|
||||
|
||||
import documentsRes from '../../plugin'
|
||||
import { $controlledDocument as controlledDocument, $isEditable as isEditable } from '../../stores/editors/document'
|
||||
import DocumentBoxItems from '../DocumentBoxItems.svelte'
|
||||
|
||||
const client = getClient()
|
||||
let changeControl: ChangeControl
|
||||
const ccQuery = createQuery()
|
||||
|
||||
$: if ($controlledDocument != null) {
|
||||
ccQuery.query(documents.class.ChangeControl, { _id: $controlledDocument.changeControl }, (res) => {
|
||||
;[changeControl] = res
|
||||
})
|
||||
} else {
|
||||
ccQuery.unsubscribe()
|
||||
}
|
||||
|
||||
const spacesQuery = createQuery()
|
||||
let docSpaces: Ref<DocumentSpace>[] = []
|
||||
spacesQuery.query(documents.class.DocumentSpace, {}, (res) => {
|
||||
docSpaces = res.map((s) => s._id)
|
||||
})
|
||||
|
||||
async function handleFieldUpdated (field: keyof ChangeControl, ev: UIEvent): Promise<void> {
|
||||
if (ev == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = ev.target as HTMLInputElement
|
||||
|
||||
if (target == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await updateCCField(field, target.value)
|
||||
}
|
||||
|
||||
async function updateCCField<T extends keyof ChangeControl> (field: T, value: ChangeControl[T]): Promise<void> {
|
||||
if ($controlledDocument == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(
|
||||
documents.class.ChangeControl,
|
||||
$controlledDocument.space,
|
||||
$controlledDocument.changeControl,
|
||||
{ [field]: value }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $controlledDocument !== undefined && changeControl}
|
||||
<Scroller>
|
||||
<div class="root">
|
||||
<div class="block">
|
||||
<div class="title">
|
||||
<Label label={documents.string.Description} />
|
||||
</div>
|
||||
{#if $isEditable}
|
||||
<PlainTextEditor
|
||||
value={changeControl.description}
|
||||
placeholder={documentsRes.string.DescribeChanges}
|
||||
disabled={!$isEditable}
|
||||
on:blur={(ev) => {
|
||||
void handleFieldUpdated('description', ev)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{changeControl.description ?? '—'}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">
|
||||
<Label label={documents.string.Reason} />
|
||||
</div>
|
||||
{#if $isEditable}
|
||||
<PlainTextEditor
|
||||
value={changeControl.reason}
|
||||
placeholder={documentsRes.string.DescribeReason}
|
||||
disabled={!$isEditable}
|
||||
on:blur={(ev) => {
|
||||
void handleFieldUpdated('reason', ev)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{changeControl.reason ?? '—'}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">
|
||||
<Label label={documents.string.ImpactAnalysis} />
|
||||
</div>
|
||||
{#if $isEditable}
|
||||
<PlainTextEditor
|
||||
value={changeControl.impact}
|
||||
placeholder={documentsRes.string.DescribeImpact}
|
||||
disabled={!$isEditable}
|
||||
on:blur={(ev) => {
|
||||
void handleFieldUpdated('impact', ev)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{changeControl.impact ?? '—'}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="title">
|
||||
<Label label={documents.string.ImpactedDocuments} />
|
||||
</div>
|
||||
{#if $isEditable || changeControl.impactedDocuments.length > 0}
|
||||
<DocumentBoxItems
|
||||
_class={documents.class.ControlledDocument}
|
||||
items={changeControl.impactedDocuments}
|
||||
label={documents.string.ImpactedDocuments}
|
||||
readonly={!$isEditable}
|
||||
docQuery={{
|
||||
space: { $in: docSpaces },
|
||||
state: DocumentState.Effective,
|
||||
attachedTo: { $ne: $controlledDocument?.attachedTo }
|
||||
}}
|
||||
on:update={({ detail }) => updateCCField('impactedDocuments', detail)}
|
||||
/>
|
||||
{:else}
|
||||
<Label label={documentsRes.string.NoDocuments} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: 1.5rem 3.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: var(--body-font-size);
|
||||
color: var(--theme-caption-color);
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DateRangeMode, type MixinUpdate, Timestamp } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
DatePresenter,
|
||||
DropdownLabels,
|
||||
Label,
|
||||
DropdownTextItem,
|
||||
RadioButton,
|
||||
Scroller,
|
||||
Toggle
|
||||
} from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { UserBoxItems } from '@hcengineering/contact-resources'
|
||||
import {
|
||||
type Document,
|
||||
ControlledDocumentState,
|
||||
DEFAULT_PERIODIC_REVIEW_INTERVAL,
|
||||
DocumentState,
|
||||
type DocumentTraining,
|
||||
periodicReviewIntervals,
|
||||
ControlledDocument
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
NullablePositiveNumberEditor,
|
||||
TrainingRefEditor,
|
||||
TrainingRequestRolesEditor
|
||||
} from '@hcengineering/training-resources'
|
||||
import { createDocumentTraining, getDocumentTrainingClass, updateDocumentTraining } from '../../docutils'
|
||||
|
||||
import documentsRes from '../../plugin'
|
||||
import {
|
||||
$isDocumentOwner as isDocumentOwner,
|
||||
$controlledDocument as controlledDocument,
|
||||
$documentState as documentState,
|
||||
$documentAllVersionsDescSorted as documentAllVersionsDescSorted,
|
||||
$documentTraining as documentTraining
|
||||
} from '../../stores/editors/document/editor'
|
||||
|
||||
enum Severity {
|
||||
Minor = 'minor',
|
||||
Major = 'major'
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function changePlannedEffectiveDate (plannedEffectiveDate: Timestamp) {
|
||||
if (!$controlledDocument) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update($controlledDocument, { plannedEffectiveDate })
|
||||
}
|
||||
|
||||
let selectedDate: Timestamp =
|
||||
$controlledDocument?.plannedEffectiveDate != null && $controlledDocument?.plannedEffectiveDate > 0
|
||||
? $controlledDocument.plannedEffectiveDate
|
||||
: Date.now()
|
||||
|
||||
let selected: IntlString | undefined = undefined
|
||||
if ($controlledDocument?.plannedEffectiveDate === 0) {
|
||||
selected = documentsRes.string.EffectiveImmediately
|
||||
} else if ($controlledDocument?.plannedEffectiveDate != null) {
|
||||
selected = documentsRes.string.EffectiveOn
|
||||
}
|
||||
|
||||
async function changeSelectedDate (ev: CustomEvent) {
|
||||
if (ev.detail !== undefined) {
|
||||
selectedDate = ev.detail
|
||||
await changePlannedEffectiveDate(ev.detail)
|
||||
selected = documentsRes.string.EffectiveOn
|
||||
}
|
||||
}
|
||||
|
||||
const reviewIntervals: DropdownTextItem[] = []
|
||||
for (const interval of periodicReviewIntervals) {
|
||||
reviewIntervals.push({
|
||||
id: interval.toString(),
|
||||
label: interval.toString()
|
||||
})
|
||||
}
|
||||
|
||||
let selectedReviewInterval = $controlledDocument?.reviewInterval
|
||||
? $controlledDocument.reviewInterval.toString()
|
||||
: DEFAULT_PERIODIC_REVIEW_INTERVAL.toString()
|
||||
async function changePeriodicReviewInterval (interval?: string) {
|
||||
if ($controlledDocument == null || interval == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const reviewInterval = Number(+interval)
|
||||
if (!Number.isSafeInteger(reviewInterval)) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update($controlledDocument, { reviewInterval })
|
||||
selectedReviewInterval = interval.toString()
|
||||
}
|
||||
|
||||
$: canEdit =
|
||||
$isDocumentOwner &&
|
||||
$documentState != null &&
|
||||
![
|
||||
DocumentState.Archived,
|
||||
DocumentState.Deleted,
|
||||
DocumentState.Effective,
|
||||
ControlledDocumentState.InApproval,
|
||||
ControlledDocumentState.Approved,
|
||||
ControlledDocumentState.ToReview
|
||||
].includes($documentState)
|
||||
|
||||
const hierarchy = client.getHierarchy()
|
||||
const documentTrainingClass = getDocumentTrainingClass(hierarchy)
|
||||
|
||||
async function toggleTraining (on: boolean): Promise<void> {
|
||||
if (!$controlledDocument || !canEdit) {
|
||||
return
|
||||
}
|
||||
|
||||
if (on) {
|
||||
if ($documentTraining === null) {
|
||||
await createDocumentTraining(client, $controlledDocument, {
|
||||
enabled: true,
|
||||
training: null,
|
||||
roles: [],
|
||||
trainees: [],
|
||||
maxAttempts: null,
|
||||
dueDays: null
|
||||
})
|
||||
} else {
|
||||
await updateTraining({ enabled: true })
|
||||
}
|
||||
} else {
|
||||
await updateTraining({ enabled: false })
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTraining (update: MixinUpdate<Document, DocumentTraining>): Promise<boolean> {
|
||||
if (!$controlledDocument || !canEdit || !documentTraining) {
|
||||
return false
|
||||
}
|
||||
await updateDocumentTraining(client, $controlledDocument, update)
|
||||
return true
|
||||
}
|
||||
|
||||
$: severity = getSeverity($controlledDocument, $documentAllVersionsDescSorted)
|
||||
|
||||
function getPreviousDocument (
|
||||
document: ControlledDocument,
|
||||
allVersionsDesc: ControlledDocument[]
|
||||
): ControlledDocument | undefined {
|
||||
return allVersionsDesc.find(
|
||||
(d) => (d.major === document.major && d.minor < document.minor) || d.major < document.major
|
||||
)
|
||||
}
|
||||
|
||||
function getSeverity (document: ControlledDocument | null, allVersionsDesc: ControlledDocument[]): Severity {
|
||||
if (document == null) {
|
||||
return Severity.Minor
|
||||
}
|
||||
|
||||
const prevDocument = getPreviousDocument(document, allVersionsDesc)
|
||||
|
||||
if (prevDocument == null) {
|
||||
return document.major > 0 ? Severity.Major : Severity.Minor
|
||||
} else {
|
||||
return prevDocument.major < document.major ? Severity.Major : Severity.Minor
|
||||
}
|
||||
}
|
||||
|
||||
function getVersionForSeverity (severity: Severity): { major: number, minor: number } | undefined {
|
||||
if ($controlledDocument == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevDocument = getPreviousDocument($controlledDocument, $documentAllVersionsDescSorted)
|
||||
|
||||
if (severity === Severity.Major) {
|
||||
return {
|
||||
major: (prevDocument?.major ?? 0) + 1,
|
||||
minor: 0
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
major: prevDocument?.major ?? 0,
|
||||
minor: (prevDocument?.minor ?? 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSeverityChanged (newSeverity: Severity): Promise<void> {
|
||||
if (!canEdit || $controlledDocument == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const oldSeverity = getSeverity($controlledDocument, $documentAllVersionsDescSorted)
|
||||
if (oldSeverity === newSeverity) {
|
||||
return
|
||||
}
|
||||
|
||||
const versionUpdate = getVersionForSeverity(newSeverity)
|
||||
if (versionUpdate === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update($controlledDocument, versionUpdate)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
<div class="content">
|
||||
<section class="section">
|
||||
<header class="fs-title text-lg mb-4">
|
||||
<Label label={documentsRes.string.ChangeSeverity} />
|
||||
</header>
|
||||
<div class="flex-col">
|
||||
<RadioButton
|
||||
group={severity}
|
||||
id={Severity.Minor}
|
||||
labelIntl={documentsRes.string.Minor}
|
||||
labelGap="large"
|
||||
value={Severity.Minor}
|
||||
disabled={!canEdit}
|
||||
action={() => {
|
||||
void handleSeverityChanged(Severity.Minor)
|
||||
}}
|
||||
gap="large"
|
||||
/>
|
||||
<RadioButton
|
||||
group={severity}
|
||||
id={Severity.Major}
|
||||
labelIntl={documentsRes.string.Major}
|
||||
labelGap="large"
|
||||
value={Severity.Major}
|
||||
disabled={!canEdit}
|
||||
action={() => {
|
||||
void handleSeverityChanged(Severity.Major)
|
||||
}}
|
||||
gap="none"
|
||||
/>
|
||||
</div>
|
||||
<header class="fs-title text-lg my-4">
|
||||
<Label label={documentsRes.string.EffectiveDocumentLifecycle} />
|
||||
</header>
|
||||
<span class="fs-title text-normal">
|
||||
<Label label={documentsRes.string.EffectiveDate} />
|
||||
</span>
|
||||
<div>
|
||||
<div class="flex-col">
|
||||
<RadioButton
|
||||
bind:group={selected}
|
||||
id={documentsRes.string.EffectiveImmediately}
|
||||
labelIntl={documentsRes.string.EffectiveImmediately}
|
||||
labelGap="large"
|
||||
value={documentsRes.string.EffectiveImmediately}
|
||||
disabled={!canEdit}
|
||||
action={() => {
|
||||
selected = documentsRes.string.EffectiveImmediately
|
||||
changePlannedEffectiveDate(0)
|
||||
}}
|
||||
gap="large"
|
||||
/>
|
||||
<RadioButton
|
||||
bind:group={selected}
|
||||
id={documentsRes.string.EffectiveOn}
|
||||
labelGap="large"
|
||||
value={documentsRes.string.EffectiveOn}
|
||||
disabled={!canEdit}
|
||||
action={() => {
|
||||
selected = documentsRes.string.EffectiveOn
|
||||
changePlannedEffectiveDate(selectedDate)
|
||||
}}
|
||||
gap="none"
|
||||
>
|
||||
<div class="flex-row-center flex-gap-1">
|
||||
<Label label={documentsRes.string.EffectiveOn} />
|
||||
<DatePresenter
|
||||
mode={DateRangeMode.DATETIME}
|
||||
bind:value={selectedDate}
|
||||
on:change={changeSelectedDate}
|
||||
editable={canEdit}
|
||||
/>
|
||||
</div>
|
||||
</RadioButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<span class="whitespace-nowrap fs-title text-normal">
|
||||
<Label label={documentsRes.string.PeriodicReviewToBeCompleted} />
|
||||
</span>
|
||||
<DropdownLabels
|
||||
kind="regular"
|
||||
selected={selectedReviewInterval}
|
||||
placeholder={documentsRes.string.PeriodicReviewToBeCompleted}
|
||||
items={reviewIntervals}
|
||||
enableSearch={false}
|
||||
multiselect={false}
|
||||
allowDeselect={false}
|
||||
on:selected={({ detail }) => changePeriodicReviewInterval(detail)}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
<span>
|
||||
<Label label={documentsRes.string.MonthsAfterEffectiveDate} />
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section pb-16">
|
||||
<header class="flex-row-center mb-4 flex-gap-4">
|
||||
<span class="fs-title text-lg">
|
||||
<Label label={documentTrainingClass.label} />
|
||||
</span>
|
||||
|
||||
<Toggle
|
||||
disabled={!canEdit}
|
||||
on={$documentTraining?.enabled}
|
||||
on:change={(event) => toggleTraining(event.detail)}
|
||||
/>
|
||||
</header>
|
||||
|
||||
{#if $documentTraining !== null && $documentTraining.enabled}
|
||||
{@const trainingAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'training')}
|
||||
<span class="fs-title text-normal">
|
||||
<Label label={trainingAttribute.label} />
|
||||
</span>
|
||||
<TrainingRefEditor
|
||||
kind="regular"
|
||||
width="min-content"
|
||||
size="medium"
|
||||
readonly={!canEdit}
|
||||
value={$documentTraining.training}
|
||||
onChange={(trainingRef) => {
|
||||
void updateTraining({ training: trainingRef ?? null })
|
||||
}}
|
||||
/>
|
||||
|
||||
{@const rolesAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'roles')}
|
||||
<span class="fs-title text-normal">
|
||||
<Label label={rolesAttribute.label} />
|
||||
</span>
|
||||
<TrainingRequestRolesEditor
|
||||
kind="regular"
|
||||
width="max-content"
|
||||
value={$documentTraining.roles}
|
||||
onChange={(roles) => {
|
||||
void updateTraining({ roles })
|
||||
}}
|
||||
/>
|
||||
|
||||
{@const traineesAttribute = hierarchy.getAttribute(documentTrainingClass._id, 'trainees')}
|
||||
<span class="fs-title text-normal">
|
||||
<Label label={traineesAttribute.label} />
|
||||
</span>
|
||||
<UserBoxItems
|
||||
items={$documentTraining.trainees}
|
||||
label={traineesAttribute.label}
|
||||
readonly={!canEdit}
|
||||
size="card"
|
||||
on:update={(event) => {
|
||||
void updateTraining({ trainees: event.detail })
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="flex-row-center flex-gap-2">
|
||||
<span class="whitespace-nowrap fs-title text-normal">
|
||||
<Label label={documentsRes.string.ToBePassedWithin} />
|
||||
</span>
|
||||
<NullablePositiveNumberEditor
|
||||
kind="regular"
|
||||
width="min-content"
|
||||
value={$documentTraining.maxAttempts}
|
||||
readonly={!canEdit}
|
||||
onChange={(maxAttempts) => {
|
||||
void updateTraining({ maxAttempts })
|
||||
}}
|
||||
/>
|
||||
<Label label={documentsRes.string.AttemptsAnd} />
|
||||
<NullablePositiveNumberEditor
|
||||
kind="regular"
|
||||
width="min-content"
|
||||
value={$documentTraining.dueDays}
|
||||
readonly={!canEdit}
|
||||
onChange={(dueDays) => {
|
||||
void updateTraining({ dueDays })
|
||||
}}
|
||||
/>
|
||||
<Label label={documentsRes.string.DaysAfterEffectiveDate} />
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
gap: 2rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 1.5rem 3.25rem 4rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { ControlledDocument, ControlledDocumentState, DocumentState } from '@hcengineering/controlled-documents'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
import DocTeam from './DocTeam.svelte'
|
||||
|
||||
export let controlledDoc: ControlledDocument
|
||||
export let editable: boolean = true
|
||||
|
||||
$: canChangeCoAuthors =
|
||||
editable && controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null
|
||||
$: canChangeReviewers =
|
||||
editable && controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null
|
||||
$: canChangeApprovers =
|
||||
editable &&
|
||||
((controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null) ||
|
||||
controlledDoc.controlledState === ControlledDocumentState.InReview ||
|
||||
controlledDoc.controlledState === ControlledDocumentState.Reviewed)
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function handleUpdate ({
|
||||
detail
|
||||
}: {
|
||||
detail: { type: 'reviewers' | 'approvers', users: Ref<Employee>[] }
|
||||
}): Promise<void> {
|
||||
const { type, users } = detail
|
||||
|
||||
await client.update(controlledDoc, { [type]: users })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if controlledDoc}
|
||||
<Scroller>
|
||||
<div class="content">
|
||||
<DocTeam
|
||||
space={controlledDoc.space}
|
||||
{controlledDoc}
|
||||
{canChangeCoAuthors}
|
||||
{canChangeReviewers}
|
||||
{canChangeApprovers}
|
||||
on:update={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 1.5rem 3.25rem;
|
||||
}
|
||||
</style>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
// TODO: Refactor to use State component from 'StateTag'
|
||||
|
||||
import { DocumentStateTagType } from '../../../utils'
|
||||
|
||||
export let type: DocumentStateTagType
|
||||
</script>
|
||||
|
||||
<div class="root {type}">
|
||||
<span class="label"><slot /></span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding: 0.125rem 0.5rem;
|
||||
display: flex;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--theme-docs-contrast-color);
|
||||
width: max-content;
|
||||
|
||||
&.inProgress {
|
||||
background: var(--theme-state-primary-background-color);
|
||||
border-color: var(--theme-state-primary-border-color);
|
||||
|
||||
.label {
|
||||
color: var(--theme-state-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.draft {
|
||||
background: var(--theme-state-regular-background-color);
|
||||
border-color: var(--theme-state-regular-border-color);
|
||||
|
||||
.label {
|
||||
color: var(--theme-state-regular-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.obsolete {
|
||||
background: var(--theme-state-ghost-background-color);
|
||||
border-color: var(--theme-state-ghost-border-color);
|
||||
|
||||
.label {
|
||||
color: var(--theme-state-ghost-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.effective {
|
||||
background: var(--theme-state-positive-background-color);
|
||||
border-color: var(--theme-state-positive-border-color);
|
||||
|
||||
.label {
|
||||
color: var(--theme-state-positive-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: var(--theme-state-negative-background-color);
|
||||
border-color: var(--theme-state-negative-border-color);
|
||||
|
||||
.label {
|
||||
color: var(--theme-state-negative-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1rem;
|
||||
}
|
||||
</style>
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Document } from '@hcengineering/controlled-documents'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { EditBox } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import plugin from '../../../plugin'
|
||||
|
||||
export let value: Document | undefined
|
||||
export let readonly = true
|
||||
|
||||
let abstract = value?.abstract
|
||||
const client = getClient()
|
||||
|
||||
const handleUpdateAbstract = () => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (value.abstract === abstract) {
|
||||
return
|
||||
}
|
||||
|
||||
client.update(value, { abstract }, true)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<EditBox
|
||||
value={abstract}
|
||||
disabled={readonly}
|
||||
placeholder={readonly ? view.string.LabelNA : plugin.string.AbstractPlaceholder}
|
||||
fullSize
|
||||
on:value={(event) => {
|
||||
abstract = event.detail
|
||||
}}
|
||||
on:blur={handleUpdateAbstract}
|
||||
/>
|
||||
{/if}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentsDocumentSection, DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { Attachments } from '@hcengineering/attachment-resources'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
|
||||
export let value: DocumentSection
|
||||
export let readonly = false
|
||||
export let showHeader = true
|
||||
export let withScroll = true
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', {})
|
||||
})
|
||||
|
||||
$: attachmentSection = value as AttachmentsDocumentSection
|
||||
$: attachmentsProps = {
|
||||
objectId: value._id,
|
||||
space: value.space,
|
||||
_class: value._class,
|
||||
attachments: attachmentSection.attachments ?? 0,
|
||||
readonly,
|
||||
showHeader
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if withScroll}
|
||||
<Scroller autoscroll>
|
||||
<div class="mr-6">
|
||||
<Attachments {...attachmentsProps} />
|
||||
</div>
|
||||
</Scroller>
|
||||
{:else}
|
||||
<Attachments {...attachmentsProps} />
|
||||
{/if}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
<!--
|
||||
// Copyright © 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { merge } from 'effector'
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||
import { CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { navigate } from '@hcengineering/ui'
|
||||
import { CollaborativeDoc, Ref, generateId, Blob } from '@hcengineering/core'
|
||||
import view from '@hcengineering/view'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
CollaboratorEditor,
|
||||
FocusExtension,
|
||||
Heading,
|
||||
HeadingsExtension,
|
||||
IsEmptyContentExtension,
|
||||
NodeHighlightExtension,
|
||||
NodeHighlightType,
|
||||
TextNodeAction,
|
||||
highlightUpdateCommand
|
||||
} from '@hcengineering/text-editor'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
|
||||
import {
|
||||
$documentCommentHighlightedLocation as highlighted,
|
||||
$areDocumentCommentPopupsOpened as arePopupsOpened,
|
||||
$canAddDocumentComments as canAddDocumentComments,
|
||||
$canViewDocumentComments as canViewDocumentComments,
|
||||
$controlledDocument as controlledDocument,
|
||||
$controlledDocumentTemplate as controlledDocumentTemplate,
|
||||
$groupedDocumentComments as groupedDocumentComments,
|
||||
$isEditable as isEditable,
|
||||
documentCommentsDisplayRequested,
|
||||
documentCommentsHighlightUpdated,
|
||||
documentCommentsLocationNavigateRequested,
|
||||
showAddCommentPopupFx
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
export let value: CollaborativeDocumentSection
|
||||
export let onHeadings: (headings: Heading[]) => void
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const user = getCollaborationUser()
|
||||
|
||||
let selectedNodeId: string | null | undefined = undefined
|
||||
let textEditor: CollaboratorEditor
|
||||
let isFocused = false
|
||||
let isEmpty = true
|
||||
|
||||
const handleRefreshHighlight = () => {
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
textEditor.commands()?.command(highlightUpdateCommand())
|
||||
}
|
||||
|
||||
const unsubscribeHighlightRefresh = merge([highlighted, groupedDocumentComments.updates]).subscribe({
|
||||
next: () => {
|
||||
handleRefreshHighlight()
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribeNavigateToLocation = documentCommentsLocationNavigateRequested.subscribe({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
next: async ({ nodeId, sectionKey }) => {
|
||||
if (sectionKey !== value.key || !nodeId) {
|
||||
handleRefreshHighlight()
|
||||
return
|
||||
}
|
||||
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
await tick()
|
||||
|
||||
const element = textEditor.getNodeElement(nodeId)
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleGetNodeId = () => {
|
||||
if (isEmpty || !textEditor) {
|
||||
return null
|
||||
}
|
||||
if (!selectedNodeId) {
|
||||
const nodeId = generateId()
|
||||
if (textEditor.setNodeUuid(nodeId)) {
|
||||
selectedNodeId = nodeId
|
||||
}
|
||||
}
|
||||
|
||||
return selectedNodeId
|
||||
}
|
||||
|
||||
const handleNodeHighlight = (id: string) => {
|
||||
if ($highlighted) {
|
||||
const { sectionKey, nodeId } = $highlighted
|
||||
if (nodeId === id && sectionKey === value.key) {
|
||||
return { type: NodeHighlightType.WARNING, isActive: true }
|
||||
}
|
||||
}
|
||||
|
||||
if ($groupedDocumentComments.hasDocumentComments(value.key, id)) {
|
||||
return { type: NodeHighlightType.WARNING }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const handleCommentAction = (): void => {
|
||||
if (isEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$canAddDocumentComments) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedNodeId) {
|
||||
selectedNodeId = handleGetNodeId()
|
||||
}
|
||||
|
||||
if (selectedNodeId) {
|
||||
showAddCommentPopupFx({
|
||||
element: textEditor.getNodeElement(selectedNodeId),
|
||||
nodeId: selectedNodeId,
|
||||
sectionKey: value.key
|
||||
})
|
||||
|
||||
textEditor.selectNode(selectedNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowDocumentComments = (uuid: string) => {
|
||||
if (!uuid) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!$groupedDocumentComments.hasDocumentComments(value.key, uuid)) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsDisplayRequested({
|
||||
element: textEditor.getNodeElement(uuid),
|
||||
nodeId: uuid,
|
||||
sectionKey: value.key
|
||||
})
|
||||
}
|
||||
|
||||
const handleExtensions = () => [
|
||||
FocusExtension.configure({
|
||||
onFocus (focused) {
|
||||
isFocused = focused
|
||||
}
|
||||
}),
|
||||
IsEmptyContentExtension.configure({
|
||||
onChange (empty) {
|
||||
isEmpty = empty
|
||||
}
|
||||
}),
|
||||
HeadingsExtension.configure({
|
||||
prefixId: `section-${value._id}`,
|
||||
onChange: onHeadings
|
||||
}),
|
||||
NodeHighlightExtension.configure({
|
||||
isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments,
|
||||
getNodeHighlight: handleNodeHighlight,
|
||||
onNodeSelected: (uuid: string | null) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
if (isFocused) {
|
||||
documentCommentsHighlightUpdated(
|
||||
selectedNodeId && $groupedDocumentComments.hasDocumentComments(value.key, uuid)
|
||||
? { sectionKey: value.key, nodeId: selectedNodeId }
|
||||
: null
|
||||
)
|
||||
}
|
||||
},
|
||||
onNodeClicked: (uuid: string) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
|
||||
if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) {
|
||||
handleShowDocumentComments(selectedNodeId)
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
async function createEmbedding (
|
||||
file: File,
|
||||
section: CollaborativeDocumentSection
|
||||
): Promise<{ file: Ref<Blob>, type: string } | undefined> {
|
||||
if ($controlledDocument === undefined || $controlledDocument === null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||
const uuid = await uploadFile(file)
|
||||
const attachmentId: Ref<Attachment> = generateId()
|
||||
|
||||
await client.addCollection(
|
||||
attachment.class.Attachment,
|
||||
section.space,
|
||||
section._id,
|
||||
section._class,
|
||||
'attachments',
|
||||
{
|
||||
file: uuid,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
},
|
||||
attachmentId
|
||||
)
|
||||
|
||||
return { file: uuid, type: file.type }
|
||||
} catch (err: any) {
|
||||
await setPlatformStatus(unknownError(err))
|
||||
} finally {
|
||||
dispatch('change')
|
||||
}
|
||||
}
|
||||
|
||||
const commentAction: TextNodeAction = {
|
||||
id: '#comment',
|
||||
icon: chunter.icon.Chunter,
|
||||
label: chunter.string.Message,
|
||||
action: handleCommentAction
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeHighlightRefresh()
|
||||
unsubscribeNavigateToLocation()
|
||||
})
|
||||
|
||||
let collaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocument !== null) {
|
||||
collaborativeDoc = $controlledDocument.content
|
||||
}
|
||||
|
||||
let initialCollaborativeDoc: CollaborativeDoc | undefined
|
||||
$: if ($controlledDocumentTemplate !== null) {
|
||||
initialCollaborativeDoc = $controlledDocumentTemplate.content
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if collaborativeDoc}
|
||||
{#key value._id}
|
||||
<CollaboratorEditor
|
||||
bind:this={textEditor}
|
||||
{collaborativeDoc}
|
||||
{initialCollaborativeDoc}
|
||||
{user}
|
||||
readonly={!$isEditable}
|
||||
field={value.collaboratorSectionId}
|
||||
textNodeActions={$canAddDocumentComments && !isEmpty ? [commentAction] : []}
|
||||
editorAttributes={{ style: 'padding: 0 2em; margin: 0 -2em;' }}
|
||||
overflow="none"
|
||||
canShowPopups={!$arePopupsOpened}
|
||||
onExtensions={handleExtensions}
|
||||
on:open-document={async (event) => {
|
||||
const doc = await client.findOne(event.detail._class, { _id: event.detail._id })
|
||||
if (doc != null) {
|
||||
const location = await getObjectLinkFragment(client.getHierarchy(), doc, {}, view.component.EditDoc)
|
||||
navigate(location)
|
||||
}
|
||||
}}
|
||||
attachFile={async (file) => {
|
||||
return await createEmbedding(file, value)
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { afterUpdate } from 'svelte'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import documentsRes from '../../../plugin'
|
||||
|
||||
export let width: string | undefined = undefined
|
||||
export let height: string | undefined = undefined
|
||||
export let value: string | undefined = undefined
|
||||
export let placeholder: IntlString = documentsRes.string.EditDescription
|
||||
export let placeholderParam: any | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
let input: HTMLElement
|
||||
let phTraslate: string = ''
|
||||
|
||||
$: translate(placeholder, placeholderParam ?? {}).then((res) => {
|
||||
phTraslate = res
|
||||
})
|
||||
|
||||
afterUpdate(() => {
|
||||
if (value !== '') {
|
||||
input.style.height = 'auto'
|
||||
input.style.height = `${input.scrollHeight + 2}px`
|
||||
}
|
||||
})
|
||||
|
||||
$: if (!disabled && input) {
|
||||
input.focus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="textarea" style:width style:height>
|
||||
<textarea
|
||||
maxlength="240"
|
||||
rows="1"
|
||||
bind:value
|
||||
bind:this={input}
|
||||
placeholder={phTraslate}
|
||||
{disabled}
|
||||
on:keydown
|
||||
on:change
|
||||
on:keydown
|
||||
on:keypress
|
||||
on:blur
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.textarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 0.25rem;
|
||||
|
||||
textarea {
|
||||
padding: 0.62rem 1rem;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--theme-docs-description-border-color);
|
||||
border-radius: 0.375rem;
|
||||
&:disabled {
|
||||
padding: 0.25rem 0.5rem 0.38rem 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background-color: var(--theme-docs-frozen-description-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { DocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { ModernDialog, IconClose } from '@hcengineering/ui'
|
||||
import { StyledTextArea } from '@hcengineering/text-editor'
|
||||
import { MessageViewer, getClient } from '@hcengineering/presentation'
|
||||
|
||||
import documentsRes from '../../../plugin'
|
||||
import GuidanceEditing from '../../icons/GuidanceEditing.svelte'
|
||||
import { type GuidanceEditorMode } from '../../../utils'
|
||||
|
||||
export let section: DocumentSection
|
||||
export let index: number
|
||||
export let width: string | undefined = undefined
|
||||
export let mode: GuidanceEditorMode = 'readonly'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function handleCloseClick (): void {
|
||||
const reopenMode: GuidanceEditorMode | undefined = mode === 'canEdit' ? 'editing' : undefined
|
||||
close(reopenMode)
|
||||
}
|
||||
|
||||
function close (reopenMode?: GuidanceEditorMode): void {
|
||||
dispatch('close', { reopenMode, guidance: reopenMode == null ? guidance : undefined })
|
||||
}
|
||||
|
||||
export function onOutsideClick (): void {
|
||||
close()
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
|
||||
const title = `${index}. ${section.title}`
|
||||
let guidance = h.as(section, documentsRes.mixin.DocumentTemplateSection).guidance
|
||||
|
||||
let textArea: StyledTextArea
|
||||
</script>
|
||||
|
||||
<ModernDialog
|
||||
label={getEmbeddedLabel(title)}
|
||||
closeIcon={mode !== 'canEdit' ? IconClose : GuidanceEditing}
|
||||
withoutFooter
|
||||
on:close={handleCloseClick}
|
||||
{width}
|
||||
shadow={mode !== 'editing'}
|
||||
>
|
||||
{#if mode === 'editing'}
|
||||
<StyledTextArea bind:this={textArea} isScrollable={false} bind:content={guidance} showButtons={false} />
|
||||
{:else}
|
||||
<!-- TODO: use StyledTextArea.setEditable when it's fixed -->
|
||||
<MessageViewer message={guidance ?? ''} />
|
||||
{/if}
|
||||
</ModernDialog>
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||
import { Ref, generateId } from '@hcengineering/core'
|
||||
import { ReferenceInput } from '@hcengineering/text-editor'
|
||||
|
||||
import {
|
||||
addDocumentCommentFx,
|
||||
$controlledDocumentSections as controlledDocumentSections
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
export let sectionKey: string
|
||||
export let nodeId: string | undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let messageId: Ref<ChatMessage> = generateId()
|
||||
|
||||
$: section = $controlledDocumentSections.find((section) => section.key === sectionKey)
|
||||
|
||||
async function handleMessage (event: CustomEvent<string>): Promise<void> {
|
||||
const comment = await addDocumentCommentFx({ content: event.detail, section, messageId, nodeId })
|
||||
messageId = generateId()
|
||||
|
||||
dispatch('close', comment)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if section}
|
||||
<div class="text-editor-popup w-85">
|
||||
<ReferenceInput
|
||||
focusable
|
||||
kindSend="primary"
|
||||
placeholder={chunter.string.AddCommentPlaceholder}
|
||||
on:message={handleMessage}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import documents, { Document } from '@hcengineering/controlled-documents'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, EditBox, Label } from '@hcengineering/ui'
|
||||
|
||||
import IconWarning from '../../icons/IconWarning.svelte'
|
||||
import documentsRes from '../../../plugin'
|
||||
|
||||
export let object: Document
|
||||
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
let code = object.code
|
||||
|
||||
const docCodesQuery = createQuery()
|
||||
let docCodes: Record<string, string> = {}
|
||||
docCodesQuery.query(
|
||||
documents.class.Document,
|
||||
{},
|
||||
(res) => {
|
||||
docCodes = {}
|
||||
for (const doc of res) {
|
||||
if (doc._id === object._id || doc.code === '' || doc.code === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
docCodes[doc.code] = doc.title
|
||||
}
|
||||
},
|
||||
{
|
||||
projection: { code: 1, title: 1 }
|
||||
}
|
||||
)
|
||||
|
||||
$: isUnique = code != null && docCodes[code] === undefined
|
||||
$: isFilled = code != null && code !== ''
|
||||
$: isSame = object.code === code
|
||||
$: canSubmit = isFilled && isUnique && !isSame
|
||||
|
||||
async function handleSubmit (): Promise<void> {
|
||||
if (!canSubmit) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(object, { code })
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<div class="text-editor-popup min-w-112">
|
||||
<div class="p-6 bottom-divider">
|
||||
<div class="text-base font-medium primary-text-color pb-2">
|
||||
<Label label={documentsRes.string.ChangeCode} />
|
||||
</div>
|
||||
<div class="flex-column flex-gap-8 pt-6">
|
||||
<EditBox
|
||||
autoFocus
|
||||
placeholder={documentsRes.string.DocumentCodePlaceholder}
|
||||
bind:value={code}
|
||||
kind="large-style"
|
||||
/>
|
||||
{#if !isUnique}
|
||||
<div class="error">
|
||||
<IconWarning size="small" />
|
||||
<Label label={documentsRes.string.CodeInUse} />
|
||||
<span class="name">{docCodes[code]}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-center flex-gap-2 pr-6 pl-6 pt-4 pb-4">
|
||||
<Button kind="regular" label={presentation.string.Cancel} on:click={() => dispatch('close')} />
|
||||
<Button kind="primary" disabled={!canSubmit} label={presentation.string.Change} on:click={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.primary-text-color {
|
||||
color: var(--theme-text-primary-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.375rem;
|
||||
color: var(--negative-button-default);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import documents, { DocumentTemplate, TEMPLATE_PREFIX } from '@hcengineering/controlled-documents'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, EditBox, Label } from '@hcengineering/ui'
|
||||
|
||||
import IconWarning from '../../icons/IconWarning.svelte'
|
||||
import documentsRes from '../../../plugin'
|
||||
|
||||
export let object: DocumentTemplate
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
let prefix = object.docPrefix
|
||||
|
||||
let templatesCodeTitle = ''
|
||||
translate(documents.string.SysTemplate, {}).then(
|
||||
(res) => {
|
||||
templatesCodeTitle = res
|
||||
templateDocPrefixes[TEMPLATE_PREFIX] = templatesCodeTitle
|
||||
},
|
||||
(err) => {
|
||||
console.warn(`Cannot load translation for: ${documents.string.SysTemplate}. Error: ${err})`)
|
||||
}
|
||||
)
|
||||
|
||||
const templateDocPrefixesQuery = createQuery()
|
||||
let templateDocPrefixes: Record<string, string> = {}
|
||||
templateDocPrefixesQuery.query(
|
||||
documents.mixin.DocumentTemplate,
|
||||
{ _id: { $ne: object._id } },
|
||||
(res) => {
|
||||
templateDocPrefixes = { [TEMPLATE_PREFIX]: templatesCodeTitle }
|
||||
for (const t of res) {
|
||||
templateDocPrefixes[t.docPrefix] = t.title
|
||||
}
|
||||
},
|
||||
{
|
||||
projection: { docPrefix: 1, title: 1 }
|
||||
}
|
||||
)
|
||||
|
||||
$: isUnique = prefix != null && templateDocPrefixes[prefix] === undefined
|
||||
$: isFilled = prefix != null && prefix !== ''
|
||||
$: isSame = object.docPrefix === prefix
|
||||
$: canSubmit = isFilled && isUnique && !isSame
|
||||
|
||||
async function handleSubmit (): Promise<void> {
|
||||
if (!canSubmit) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateMixin(object._id, documents.class.Document, object.space, documents.mixin.DocumentTemplate, {
|
||||
docPrefix: prefix
|
||||
})
|
||||
dispatch('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<div class="text-editor-popup min-w-112">
|
||||
<div class="p-6 bottom-divider">
|
||||
<div class="text-base font-medium primary-text-color pb-2">
|
||||
<Label label={documentsRes.string.ChangePrefix} />
|
||||
</div>
|
||||
<div class="flex-column flex-gap-8 pt-6">
|
||||
<EditBox
|
||||
autoFocus
|
||||
placeholder={documentsRes.string.DocumentPrefixPlaceholder}
|
||||
bind:value={prefix}
|
||||
kind="large-style"
|
||||
/>
|
||||
{#if !isUnique}
|
||||
<div class="error">
|
||||
<IconWarning size="small" />
|
||||
<Label label={documentsRes.string.CodeInUse} />
|
||||
<span class="name">{templateDocPrefixes[prefix]}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-center flex-gap-2 pr-6 pl-6 pt-4 pb-4">
|
||||
<Button kind="regular" label={presentation.string.Cancel} on:click={() => dispatch('close')} />
|
||||
<Button kind="primary" disabled={!canSubmit} label={presentation.string.Change} on:click={handleSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.primary-text-color {
|
||||
color: var(--theme-text-primary-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.375rem;
|
||||
color: var(--negative-button-default);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import documents, { Document } from '@hcengineering/controlled-documents'
|
||||
import { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import { EmployeeBox, EmployeePresenter, personAccountByIdStore } from '@hcengineering/contact-resources'
|
||||
import core, { Ref, Space } from '@hcengineering/core'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Button, Icon, Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { canChangeDocumentOwner, isDocOwner } from '../../../utils'
|
||||
import Info from '../../icons/Info.svelte'
|
||||
import DocumentVersionPresenter from '../presenters/DocumentVersionPresenter.svelte'
|
||||
import StatePresenter from '../presenters/StatePresenter.svelte'
|
||||
|
||||
export let object: Document
|
||||
|
||||
let space: Space | undefined
|
||||
let owner: Ref<Employee> | undefined = undefined
|
||||
let canChange = false
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const handleChangeOwner = async () => {
|
||||
if (!canChange || !owner || !object || owner === object.owner) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(object, { owner })
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
$: query.query(core.class.Space, { _id: object.space }, (res) => {
|
||||
;[space] = res
|
||||
})
|
||||
|
||||
$: if (object) {
|
||||
void canChangeDocumentOwner(object).then((value) => {
|
||||
canChange = value
|
||||
})
|
||||
}
|
||||
|
||||
$: isOwner = isDocOwner(object)
|
||||
|
||||
$: members = space?.members ?? []
|
||||
$: employees = members
|
||||
.map((m) => $personAccountByIdStore.get(m as Ref<PersonAccount>)?.person as Ref<Employee>)
|
||||
.filter((p) => p !== undefined)
|
||||
|
||||
$: docQuery = space?.private ?? false ? { active: true, _id: { $in: employees } } : { active: true }
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<div class="text-editor-popup min-w-112">
|
||||
<div class="p-6 bottom-divider">
|
||||
<div class="text-base font-medium primary-text-color pb-2">
|
||||
<Label label={documents.string.ChangeOwner} />
|
||||
</div>
|
||||
<div class="flex flex-gap-1 hint text-sm">
|
||||
<Label label={documents.string.ChangeOwnerHintBeginning} />
|
||||
<div class="flex flex-gap-1 primary-text-color fs-bold">
|
||||
<span>{object.title}</span>
|
||||
<div><DocumentVersionPresenter value={object} /></div>
|
||||
<div>[<StatePresenter value={object} showTag={false} />]</div>
|
||||
</div>
|
||||
<Label label={documents.string.ChangeOwnerHintEnd} />
|
||||
</div>
|
||||
<div class="flex flex-gap-4 items-center pt-6">
|
||||
{#if !isOwner}
|
||||
<div class="fs-bold primary-text-color">
|
||||
<EmployeePresenter value={object.owner} avatarSize="card" noUnderline disabled colorInherit />
|
||||
</div>
|
||||
<Icon icon={view.icon.ArrowRight} size="medium" fill="var(--theme-progress-color)" />
|
||||
{/if}
|
||||
<EmployeeBox
|
||||
bind:value={owner}
|
||||
{docQuery}
|
||||
label={documents.string.SelectOwner}
|
||||
readonly={!canChange}
|
||||
showNavigate={false}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center flex-between pr-6 pl-6 pt-4 pb-4">
|
||||
<div class="flex flex-gap-2 items-center max-w-60 p-1 text-xs pr-4">
|
||||
{#if isOwner}
|
||||
<div class="warning-sign">
|
||||
<Info size="small" />
|
||||
</div>
|
||||
<Label label={documents.string.ChangeOwnerWarning} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end items-center flex-gap-2">
|
||||
<Button kind="regular" label={presentation.string.Cancel} on:click={() => dispatch('close')} />
|
||||
<Button
|
||||
kind={!isOwner ? 'primary' : 'dangerous'}
|
||||
disabled={!canChange || !owner || owner === object.owner}
|
||||
label={presentation.string.Change}
|
||||
on:click={handleChangeOwner}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.hint {
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
|
||||
.warning-sign {
|
||||
color: var(--theme-docs-warning-icon-color);
|
||||
}
|
||||
|
||||
.primary-text-color {
|
||||
color: var(--theme-text-primary-color);
|
||||
}
|
||||
</style>
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { DropdownIntlItem, DropdownLabelsIntl, Label, Toggle } from '@hcengineering/ui'
|
||||
import documents, { type DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
$documentCommentsFilter as documentCommentsFilter,
|
||||
documentCommentsShowResolvedToggled,
|
||||
documentCommentsSortByChanged,
|
||||
documentCommentsSortingAttributes
|
||||
} from '../../../stores/editors/document'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
const sortingOptions: DropdownIntlItem[] = documentCommentsSortingAttributes.map((attr) => {
|
||||
const attribute = hierarchy.getAttribute(documents.class.DocumentComment, attr)
|
||||
return {
|
||||
id: attr,
|
||||
label: attribute.label
|
||||
}
|
||||
})
|
||||
|
||||
const handleShowResolvedToggled = (): void => {
|
||||
documentCommentsShowResolvedToggled()
|
||||
}
|
||||
|
||||
const handleSortingChanged = (event: CustomEvent<keyof DocumentComment>) => {
|
||||
documentCommentsSortByChanged(event.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="antiCard dialog menu">
|
||||
<div class="antiCard-menu__spacer" />
|
||||
<div
|
||||
class="antiCard-menu__item hoverable ordering"
|
||||
on:click={handleShowResolvedToggled}
|
||||
on:keydown={handleShowResolvedToggled}
|
||||
>
|
||||
<span class="overflow-label"><Label label={documents.string.ShowResolved} /></span>
|
||||
|
||||
<Toggle on={$documentCommentsFilter.showResolved} on:change={handleShowResolvedToggled} />
|
||||
</div>
|
||||
<div class="antiCard-menu__item ordering">
|
||||
<span class="overflow-label"><Label label={documents.string.Ordering} /></span>
|
||||
<DropdownLabelsIntl
|
||||
kind={'regular'}
|
||||
size={'medium'}
|
||||
items={sortingOptions}
|
||||
selected={$documentCommentsFilter.sortBy}
|
||||
justify="left"
|
||||
on:selected={handleSortingChanged}
|
||||
/>
|
||||
</div>
|
||||
<div class="antiCard-menu__spacer" />
|
||||
</div>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { $groupedDocumentComments as groupedDocumentComments } from '../../../stores/editors/document'
|
||||
import DocumentCommentThread from '../right-panel/DocumentCommentThread.svelte'
|
||||
|
||||
export let sectionKey: string
|
||||
export let nodeId: string | undefined
|
||||
|
||||
$: documentComments = $groupedDocumentComments.getDocumentComments(sectionKey, nodeId)
|
||||
</script>
|
||||
|
||||
<div class="text-editor-popup max-h-80 min-w-100 overflow-y-auto">
|
||||
{#each documentComments as object}
|
||||
<DocumentCommentThread value={object} />
|
||||
{/each}
|
||||
</div>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentsDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getPanelURI, Icon } from '@hcengineering/ui'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<AttachmentsDocumentSection> | undefined
|
||||
export let inline = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href="#{getPanelURI(documents.component.EditDoc, value.attachedTo, value.attachedToClass, 'content')}"
|
||||
class:inline-presenter={inline}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={documents.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap">{value.title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import documents, { DocumentCategory } from '@hcengineering/controlled-documents'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
export let value: Ref<DocumentCategory> | undefined
|
||||
|
||||
let category: DocumentCategory | undefined = undefined
|
||||
const client = getClient()
|
||||
|
||||
$: if (value) {
|
||||
client.findOne(documents.class.DocumentCategory, { _id: value }).then((result) => {
|
||||
category = result
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if category}
|
||||
{category.title}
|
||||
{:else}
|
||||
<Label label={view.string.LabelNA} />
|
||||
{/if}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { CollaborativeDocumentSection } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getPanelURI, Icon } from '@hcengineering/ui'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<CollaborativeDocumentSection> | undefined
|
||||
export let inline = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href="#{getPanelURI(documents.component.EditDoc, value.attachedTo, value.attachedToClass, 'content')}"
|
||||
class:inline-presenter={inline}
|
||||
>
|
||||
<div class="icon">
|
||||
<Icon icon={documents.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label nowrap">{value.title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { ControlledDocumentState } from '@hcengineering/controlled-documents'
|
||||
import { controlledDocumentStatesOrder } from '../../../utils'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
|
||||
export let value: Map<number, Map<ControlledDocumentState, ControlledDocumentState[]>>
|
||||
|
||||
let states: ControlledDocumentState[] = []
|
||||
$: states = Array.from(value.values())
|
||||
.map(([_, states]) => states[0])
|
||||
.sort(
|
||||
(state1, state2) => controlledDocumentStatesOrder.indexOf(state1) - controlledDocumentStatesOrder.indexOf(state2)
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex-presenter flex-gap-1-5">
|
||||
{#each states as state}
|
||||
<StatePresenter value={state} />
|
||||
{/each}
|
||||
</div>
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type DocumentTemplate } from '@hcengineering/controlled-documents'
|
||||
|
||||
import { Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import ChangeDocPrefixPopup from '../popups/ChangeDocPrefixPopup.svelte'
|
||||
|
||||
export let value: DocumentTemplate
|
||||
export let editable: boolean = false
|
||||
|
||||
function handleClick (event: MouseEvent): void {
|
||||
if (!editable) {
|
||||
return
|
||||
}
|
||||
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
|
||||
showPopup(
|
||||
ChangeDocPrefixPopup,
|
||||
{
|
||||
object: value
|
||||
},
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter inline-presenter noBold"
|
||||
class:no-underline={!editable}
|
||||
class:cursor-inherit={!editable}
|
||||
href={undefined}
|
||||
on:click={handleClick}
|
||||
>
|
||||
{value.docPrefix}
|
||||
</a>
|
||||
{:else}
|
||||
<Label label={view.string.LabelNA} />
|
||||
{/if}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Document } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { getPanelURI, tooltip } from '@hcengineering/ui'
|
||||
|
||||
import DocumentIcon from '../../icons/DocumentIcon.svelte'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<Document> | undefined
|
||||
export let inline = false
|
||||
export let isGray = false
|
||||
export let isRegular = false
|
||||
export let withIcon = false
|
||||
export let withTitle = false
|
||||
export let disableLink = false
|
||||
export let editable = false
|
||||
|
||||
$: documentCode = value !== undefined ? value.code : ''
|
||||
$: noUnderline = disableLink && !editable
|
||||
$: title = withTitle ? `${documentCode} ${value?.title}` : documentCode
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function handleClick (event: MouseEvent): void {
|
||||
if (!editable) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('edit', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href={!disableLink ? `#${getPanelURI(documents.component.EditDoc, value._id, value._class, 'content')}` : undefined}
|
||||
class:inline-presenter={inline}
|
||||
class:noBold={isRegular}
|
||||
class:no-underline={noUnderline}
|
||||
class:cursor-inherit={noUnderline}
|
||||
use:tooltip={{ label: getEmbeddedLabel(title) }}
|
||||
on:click={handleClick}
|
||||
>
|
||||
{#if withIcon}
|
||||
<div class="icon">
|
||||
<DocumentIcon size="small" />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="label nowrap" class:no-underline={noUnderline} class:label-gray={isGray}>{title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.label-gray {
|
||||
color: var(--theme-halfcontent-color);
|
||||
}
|
||||
</style>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import documents, { Document } from '@hcengineering/controlled-documents'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
export let value: Ref<Document> | undefined
|
||||
|
||||
let document: Document | undefined = undefined
|
||||
const client = getClient()
|
||||
|
||||
$: if (value) {
|
||||
client.findOne(documents.class.Document, { _id: value }).then((result) => {
|
||||
document = result
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if document}
|
||||
{document.title}
|
||||
{:else}
|
||||
<Label label={view.string.LabelNA} />
|
||||
{/if}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Document } from '@hcengineering/controlled-documents'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import document from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<Document> | undefined
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Label label={document.string.VersionValue} params={{ major: value.major, minor: value.minor }} />
|
||||
{/if}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import documents, { Document } from '@hcengineering/controlled-documents'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { PersonPresenter } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { checkMyPermission, permissionsStore } from '@hcengineering/view-resources'
|
||||
|
||||
import document from '../../../plugin'
|
||||
import ChangeOwnerPopup from '../popups/ChangeOwnerPopup.svelte'
|
||||
import { getCurrentEmployee } from '../../../utils'
|
||||
|
||||
export let _id: Ref<Employee> | undefined
|
||||
export let value: Employee | null | undefined
|
||||
export let object: Document
|
||||
export let isEditable: boolean = false
|
||||
export let shouldShowLabel: boolean = false
|
||||
export let defaultName: IntlString | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const me = getCurrentEmployee()
|
||||
|
||||
$: canChangeOwner =
|
||||
isEditable &&
|
||||
(object.owner === me ||
|
||||
checkMyPermission(documents.permission.UpdateDocumentOwner, object.space, $permissionsStore))
|
||||
|
||||
const handleOwnerChanged = async (result: Employee | null | undefined) => {
|
||||
if (!isEditable || !result || result._id === object.owner) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.update(object, { owner: result._id })
|
||||
}
|
||||
|
||||
const handleOwnerEditorOpened = async (event: MouseEvent) => {
|
||||
if (!canChangeOwner) {
|
||||
return
|
||||
}
|
||||
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
|
||||
showPopup(
|
||||
ChangeOwnerPopup,
|
||||
{
|
||||
object
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
handleOwnerChanged
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<PersonPresenter
|
||||
{defaultName}
|
||||
value={_id ?? value?._id}
|
||||
disabled={!canChangeOwner}
|
||||
avatarSize={'x-small'}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
tooltipLabels={{ personLabel: document.string.AssignedTo, placeholderLabel: document.string.Unassigned }}
|
||||
onEdit={canChangeOwner ? handleOwnerEditorOpened : undefined}
|
||||
/>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { DocumentState } from '@hcengineering/controlled-documents'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
import { documentStatesOrder } from '../../../utils'
|
||||
|
||||
export let value: Map<number, Map<DocumentState, DocumentState[]>>
|
||||
|
||||
let states: DocumentState[] = []
|
||||
$: states = Array.from(value.values())
|
||||
.map(([_, states]) => states[0])
|
||||
.sort((state1, state2) => documentStatesOrder.indexOf(state1) - documentStatesOrder.indexOf(state2))
|
||||
</script>
|
||||
|
||||
<div class="flex-presenter flex-gap-1-5">
|
||||
{#each states as state}
|
||||
<StatePresenter value={state} />
|
||||
{/each}
|
||||
</div>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
import {
|
||||
ControlledDocument,
|
||||
ControlledDocumentState,
|
||||
Document,
|
||||
DocumentState,
|
||||
isControlledDocument
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import DocumentStatusTag from '../common/DocumentStatusTag.svelte'
|
||||
import {
|
||||
statesTags,
|
||||
controlledStatesTags,
|
||||
DocumentStateTagType,
|
||||
TranslatedControlledDocStates,
|
||||
TranslatedDocumentStates,
|
||||
getTranslatedControlledDocStates,
|
||||
getTranslatedDocumentStates
|
||||
} from '../../../utils'
|
||||
|
||||
export let value: WithLookup<Document> | WithLookup<ControlledDocument> | ControlledDocumentState | DocumentState
|
||||
export let showTag = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let docStates: TranslatedDocumentStates | undefined
|
||||
let controlledDocStates: TranslatedControlledDocStates | undefined
|
||||
|
||||
async function getTranslatedLabels (lang: string) {
|
||||
docStates = await getTranslatedDocumentStates(lang)
|
||||
controlledDocStates = await getTranslatedControlledDocStates(lang)
|
||||
}
|
||||
|
||||
$: getTranslatedLabels($themeStore.language)
|
||||
|
||||
let text: string = ''
|
||||
let type: DocumentStateTagType
|
||||
|
||||
$: if (docStates && controlledDocStates) {
|
||||
if (typeof value === 'string') {
|
||||
text = controlledDocStates[value as ControlledDocumentState] || docStates[value as DocumentState]
|
||||
type = controlledStatesTags[value as ControlledDocumentState] || statesTags[value as DocumentState]
|
||||
} else if (isControlledDocument(client, value) && value.controlledState != null) {
|
||||
text = controlledDocStates[value.controlledState]
|
||||
type = controlledStatesTags[value.controlledState]
|
||||
} else {
|
||||
text = docStates[value.state]
|
||||
type = statesTags[value.state]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if docStates !== undefined && controlledDocStates !== undefined}
|
||||
{#if showTag}
|
||||
<DocumentStatusTag {type}>{text}</DocumentStatusTag>
|
||||
{:else}
|
||||
{text}
|
||||
{/if}
|
||||
{/if}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Document } from '@hcengineering/controlled-documents'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getPanelURI } from '@hcengineering/ui'
|
||||
import documents from '../../../plugin'
|
||||
|
||||
export let value: WithLookup<Document>
|
||||
export let inline = false
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<a
|
||||
class="flex-presenter"
|
||||
href="#{getPanelURI(documents.component.EditDoc, value._id, value._class, 'content')}"
|
||||
class:inline-presenter={inline}
|
||||
>
|
||||
<span class="label nowrap">{value.title}</span>
|
||||
</a>
|
||||
{/if}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import documents from '@hcengineering/controlled-documents'
|
||||
import { Label, Button, showPopup } from '@hcengineering/ui'
|
||||
|
||||
import TeamPopup from '../../TeamPopup.svelte'
|
||||
import {
|
||||
$canSendForApproval as canSendForApproval,
|
||||
$controlledDocument as controlledDocument
|
||||
} from '../../../stores/editors/document'
|
||||
import documentsRes from '../../../plugin'
|
||||
import { TeamPopupData } from '../../../utils'
|
||||
|
||||
function onSendDocRequest (): void {
|
||||
if ($controlledDocument == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const teamPopupData: TeamPopupData = {
|
||||
controlledDoc: $controlledDocument,
|
||||
requestClass: documents.class.DocumentApprovalRequest
|
||||
}
|
||||
|
||||
showPopup(TeamPopup, teamPopupData, 'center')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="add-approval-message">
|
||||
<div class="message-title"><Label label={documentsRes.string.AddApprovalTitle} /></div>
|
||||
<div><Label label={documentsRes.string.AddApprovalDescription1} /></div>
|
||||
<ul>
|
||||
<li><Label label={documentsRes.string.AddApprovalDescription2} /></li>
|
||||
<li><Label label={documentsRes.string.AddApprovalDescription3} /></li>
|
||||
<li><Label label={documentsRes.string.AddApprovalDescription4} /></li>
|
||||
</ul>
|
||||
</div>
|
||||
<Button
|
||||
label={documentsRes.string.SendForApproval}
|
||||
disabled={!$canSendForApproval}
|
||||
kind="regular"
|
||||
size="medium"
|
||||
on:click={() => {
|
||||
onSendDocRequest()
|
||||
}}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
.add-approval-message {
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
ul {
|
||||
margin-block-start: 0.25rem;
|
||||
margin-block-end: 0;
|
||||
padding-inline-start: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.message-title {
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import documents, { DocumentRequest } from '@hcengineering/controlled-documents'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { PersonAccountRefPresenter } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Chevron, Label } from '@hcengineering/ui'
|
||||
|
||||
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 WaitingIcon from '../../icons/Waiting.svelte'
|
||||
|
||||
export let request: DocumentRequest
|
||||
export let initiallyExpanded: boolean = false
|
||||
|
||||
interface PersonalApproval {
|
||||
account: Ref<PersonAccount>
|
||||
approved: 'approved' | 'rejected' | 'cancelled' | 'waiting'
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let expanded: boolean = initiallyExpanded
|
||||
|
||||
let rejectingMessage: string | undefined
|
||||
let approvals: PersonalApproval[] = []
|
||||
|
||||
$: if (request != null) {
|
||||
void getRequestData()
|
||||
}
|
||||
|
||||
$: type = hierarchy.isDerived(request._class, documents.class.DocumentApprovalRequest)
|
||||
? documents.string.Approval
|
||||
: documents.string.Review
|
||||
|
||||
async function getRequestData (): Promise<void> {
|
||||
if (request !== undefined) {
|
||||
approvals = await getApprovals(request)
|
||||
const rejectingComment = await client.findOne(chunter.class.ChatMessage, {
|
||||
attachedTo: request?._id,
|
||||
attachedToClass: request?._class
|
||||
})
|
||||
rejectingMessage = rejectingComment?.message
|
||||
}
|
||||
}
|
||||
|
||||
async function getApprovals (req: DocumentRequest): Promise<PersonalApproval[]> {
|
||||
const rejectedBy: PersonalApproval[] =
|
||||
req.rejected !== undefined
|
||||
? [
|
||||
{
|
||||
account: req.rejected,
|
||||
approved: 'rejected'
|
||||
}
|
||||
]
|
||||
: []
|
||||
const approvedBy: PersonalApproval[] = req.approved.map((id) => ({
|
||||
account: id,
|
||||
approved: 'approved'
|
||||
}))
|
||||
const ignoredBy = req.requested
|
||||
.filter((p) => p !== req?.rejected)
|
||||
.filter((p) => !(req?.approved as string[]).includes(p))
|
||||
.map(
|
||||
(id): PersonalApproval => ({
|
||||
account: 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'
|
||||
})
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:bottom-divider={!expanded}
|
||||
class="justify-start"
|
||||
on:click={() => {
|
||||
expanded = !expanded
|
||||
}}
|
||||
>
|
||||
<div class="header flex-row-center flex-gap-1-5">
|
||||
<span class="title">
|
||||
{#if snapshot != null}
|
||||
{snapshot?.name}
|
||||
{:else}
|
||||
<Label label={documentsRes.string.CurrentVersion} />
|
||||
{/if}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span><Label label={type} /></span>
|
||||
<span>•</span>
|
||||
<span class="date">{dtf.format(request?.modifiedOn)}</span>
|
||||
<div class="chevron" class:visible={expanded}>
|
||||
<Chevron outline {expanded} size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{#if expanded}
|
||||
<div class="section" transition:slide|local>
|
||||
{#each approvals as approver}
|
||||
<div class="approver">
|
||||
<PersonAccountRefPresenter value={approver.account} avatarSize="x-small" />
|
||||
{#if approver.approved === 'approved'}
|
||||
<ApprovedIcon size="medium" fill={'var(--theme-docs-accepted-color)'} />
|
||||
{:else if approver.approved === 'rejected'}
|
||||
<RejectedIcon size="medium" fill={'var(--negative-button-default)'} />
|
||||
{:else if approver.approved === 'cancelled'}
|
||||
<CancelledIcon size="medium" />
|
||||
{:else if approver.approved === 'waiting'}
|
||||
<WaitingIcon size="medium" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if rejectingMessage !== undefined && approver.approved === 'rejected'}
|
||||
<div class="reject-message">{@html rejectingMessage}</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
button:hover {
|
||||
background-color: var(--theme-button-hovered);
|
||||
|
||||
&:hover {
|
||||
.chevron {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
padding: 0.75rem 1rem;
|
||||
color: var(--theme-text-primary-color);
|
||||
|
||||
.title {
|
||||
margin: 0 0.125rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
margin: 0 0.125rem;
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
margin-left: 0.25rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-text-primary-color);
|
||||
padding: 0.75rem 1rem 1rem 1rem;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
|
||||
.reject-message {
|
||||
font-weight: 400;
|
||||
padding: 0.625rem 1rem 0 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.approver {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import documents, {
|
||||
ControlledDocumentState,
|
||||
DocumentRequest,
|
||||
DocumentState
|
||||
} 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 DocumentApprovalGuideItem from './DocumentApprovalGuideItem.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let requests: DocumentRequest[] = []
|
||||
let approvals: DocumentRequest[] = []
|
||||
|
||||
$: approvals = requests.filter((p) => hierarchy.isDerived(p._class, documents.class.DocumentApprovalRequest))
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(
|
||||
documents.class.DocumentRequest,
|
||||
{
|
||||
_class: {
|
||||
$in: [documents.class.DocumentApprovalRequest, documents.class.DocumentReviewRequest]
|
||||
},
|
||||
attachedTo: $controlledDocument?._id
|
||||
},
|
||||
(result) => {
|
||||
requests = result
|
||||
},
|
||||
{
|
||||
sort: { createdOn: SortingOrder.Descending }
|
||||
}
|
||||
)
|
||||
|
||||
$: hasGuide =
|
||||
$controlledDocument?.state === DocumentState.Draft &&
|
||||
($controlledDocument?.controlledState == null ||
|
||||
![
|
||||
ControlledDocumentState.Approved,
|
||||
ControlledDocumentState.Rejected,
|
||||
ControlledDocumentState.InApproval
|
||||
].includes($controlledDocument?.controlledState))
|
||||
</script>
|
||||
|
||||
<RightPanelTabHeader>
|
||||
<Label label={document.string.DocumentApprovals} />
|
||||
</RightPanelTabHeader>
|
||||
|
||||
<Scroller>
|
||||
{#if hasGuide}
|
||||
<div class="p-4 bottom-divider">
|
||||
<DocumentApprovalGuideItem />
|
||||
</div>
|
||||
{/if}
|
||||
{#if requests.length > 0}
|
||||
{#each requests as object, idx}
|
||||
<DocumentApprovalItem request={object} initiallyExpanded={!hasGuide && idx === 0} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if !hasGuide && approvals.length === 0}
|
||||
<div class="no-approvals-message"><Label label={document.string.NoApprovalsDescription} /></div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.no-approvals-message {
|
||||
opacity: 0.8;
|
||||
font-weight: 400;
|
||||
width: 100%;
|
||||
color: var(--theme-text-primary-color);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2023 Xored
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import documents, { DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import { ThreadView } from '@hcengineering/chunter-resources'
|
||||
import { Button, IconCheckCircle } from '@hcengineering/ui'
|
||||
import {
|
||||
$canAddDocumentCommentsFeedback as canAddDocumentCommentsFeedback,
|
||||
$controlledDocumentSectionIds as controlledDocumentSectionIds,
|
||||
$controlledDocumentSections as controlledDocumentSections,
|
||||
resolveCommentFx
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
export let value: DocumentComment | undefined
|
||||
export let highlighted = false
|
||||
|
||||
$: section =
|
||||
value !== undefined
|
||||
? $controlledDocumentSections.find((item) => value !== undefined && item.key === value.sectionKey)
|
||||
: null
|
||||
$: sectionIndex = section != null ? $controlledDocumentSectionIds.indexOf(section._id) + 1 : null
|
||||
|
||||
const handleResolveComment = async (): Promise<void> => {
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await resolveCommentFx({ comment: value, resolved: !isResolved(value) })
|
||||
}
|
||||
|
||||
$: resolved = isResolved(value)
|
||||
function isResolved (val: DocumentComment | undefined): boolean {
|
||||
return val?.resolved !== undefined && val.resolved
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value !== undefined}
|
||||
<div class:highlighted class="root">
|
||||
<div class="header pt-2 pb-2 pl-4 pr-4 flex-between">
|
||||
<div>
|
||||
{#if value?.index}
|
||||
<span>#{value.index}</span>
|
||||
<span>•</span>
|
||||
{/if}
|
||||
{#if sectionIndex !== null}
|
||||
<span>{sectionIndex}.</span>
|
||||
{/if}
|
||||
{#if section}
|
||||
<span>{section.title}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $canAddDocumentCommentsFeedback}
|
||||
<div class="tools">
|
||||
<Button
|
||||
icon={resolved ? IconCheckCircle : documents.icon.CheckmarkCircle}
|
||||
iconProps={{ size: 'medium', fill: resolved ? 'var(--theme-docs-accepted-color)' : undefined }}
|
||||
kind="icon"
|
||||
showTooltip={{ label: resolved ? documents.string.Unresolve : documents.string.Resolve }}
|
||||
on:click={handleResolveComment}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<ThreadView _id={value._id} showHeader={false} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
.tools {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tools {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.highlighted {
|
||||
background-color: var(--theme-docs-comment-highlighted-color);
|
||||
}
|
||||
</style>
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Button, Label, showPopup } from '@hcengineering/ui'
|
||||
import documents, { type DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import { onDestroy } from 'svelte'
|
||||
import {
|
||||
$documentCommentHighlightedLocation as highlightedLocation,
|
||||
$documentComments as documentComments,
|
||||
$isDocumentCommentsFilterDirty as isFilterDirty,
|
||||
documentCommentsLocationNavigateRequested,
|
||||
documentCommentsNavigateRequested
|
||||
} from '../../../stores/editors/document'
|
||||
import { isDocumentCommentAttachedTo } from '../../../utils'
|
||||
import DocumentCommentThread from './DocumentCommentThread.svelte'
|
||||
import CommentFilterSettingsPopup from '../popups/CommentFilterSettingsPopup.svelte'
|
||||
import RightPanelTabHeader from './RightPanelTabHeader.svelte'
|
||||
|
||||
const elements: Record<Ref<DocumentComment>, HTMLElement> = {}
|
||||
|
||||
const handleScrollIntoView = (item: DocumentComment): void => {
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
|
||||
const itemElement = elements[item._id]
|
||||
if (itemElement) {
|
||||
itemElement.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribeNavigateTo = documentCommentsNavigateRequested.subscribe({
|
||||
next: ({ value }) => {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
|
||||
handleScrollIntoView(value)
|
||||
}
|
||||
})
|
||||
|
||||
const handleItemHighlight = (item: DocumentComment) => () => {
|
||||
if (item.sectionKey === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsLocationNavigateRequested({
|
||||
sectionKey: item.sectionKey,
|
||||
nodeId: item.nodeId ?? null
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenFilter = (ev?: Event) => {
|
||||
showPopup(CommentFilterSettingsPopup, {}, ev?.target as HTMLElement)
|
||||
}
|
||||
|
||||
onDestroy(unsubscribeNavigateTo)
|
||||
</script>
|
||||
|
||||
<RightPanelTabHeader>
|
||||
<div class="flex-between w-full">
|
||||
<Label label={chunter.string.Comments} />
|
||||
<div class="configure-button">
|
||||
<Button icon={documents.icon.Configure} kind="ghost" on:click={handleOpenFilter} />
|
||||
{#if $isFilterDirty}
|
||||
<div class="dirty-mark" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</RightPanelTabHeader>
|
||||
{#if $documentComments.length > 0}
|
||||
{#each $documentComments as object}
|
||||
<div
|
||||
bind:this={elements[object._id]}
|
||||
on:click={handleItemHighlight(object)}
|
||||
on:keydown={handleItemHighlight(object)}
|
||||
>
|
||||
<DocumentCommentThread
|
||||
value={object}
|
||||
highlighted={!!$highlightedLocation && isDocumentCommentAttachedTo(object, $highlightedLocation)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.configure-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dirty-mark {
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
right: 0.375rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--highlight-red);
|
||||
}
|
||||
</style>
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import documents, { type Document, type DocumentTemplate } from '@hcengineering/controlled-documents'
|
||||
import { PersonPresenter } from '@hcengineering/contact-resources'
|
||||
import { DateRangeMode } from '@hcengineering/core'
|
||||
import { DatePresenter, Label, Scroller, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
import documentsRes from '../../../plugin'
|
||||
import {
|
||||
$controlledDocument as controlledDocument,
|
||||
$isEditable as isEditable,
|
||||
$projectRef as projectRef
|
||||
} from '../../../stores/editors/document'
|
||||
|
||||
import CategoryPresenter from '../presenters/CategoryPresenter.svelte'
|
||||
import DocumentTitlePresenter from '../presenters/DocumentTitlePresenter.svelte'
|
||||
import DocumentVersionPresenter from '../presenters/DocumentVersionPresenter.svelte'
|
||||
import OwnerPresenter from '../presenters/OwnerPresenter.svelte'
|
||||
import StatePresenter from '../presenters/StatePresenter.svelte'
|
||||
import DocumentPresenter from '../presenters/DocumentPresenter.svelte'
|
||||
|
||||
import ChangeDocCodePopup from '../popups/ChangeDocCodePopup.svelte'
|
||||
import DocumentInfo from './info/DocumentInfo.svelte'
|
||||
import RightPanelTabHeader from './RightPanelTabHeader.svelte'
|
||||
import DocumentInfoLabel from './info/DocumentInfoLabel.svelte'
|
||||
import AbstractEditor from '../editors/AbstractEditor.svelte'
|
||||
import DocumentFlatHierarchy from './info/DocumentFlatHierarchy.svelte'
|
||||
import DocumentPrefixPresenter from '../presenters/DocumentPrefixPresenter.svelte'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
function handleCodeEdit (event: MouseEvent): void {
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
|
||||
showPopup(
|
||||
ChangeDocCodePopup,
|
||||
{
|
||||
object: $controlledDocument
|
||||
},
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
|
||||
$: isDocCodeEditable =
|
||||
$isEditable && $controlledDocument != null && $controlledDocument.major === 0 && $controlledDocument.minor === 0
|
||||
$: isDocPrefixEditable = isDocCodeEditable
|
||||
$: isTemplate =
|
||||
$controlledDocument != null && hierarchy.hasMixin($controlledDocument, documents.mixin.DocumentTemplate)
|
||||
|
||||
let asTemplate: DocumentTemplate
|
||||
$: if ($controlledDocument != null && isTemplate) {
|
||||
asTemplate = hierarchy.as<Document, DocumentTemplate>($controlledDocument, documents.mixin.DocumentTemplate)
|
||||
}
|
||||
</script>
|
||||
|
||||
<RightPanelTabHeader>
|
||||
<Label label={documentsRes.string.GeneralInfo} />
|
||||
</RightPanelTabHeader>
|
||||
{#if $controlledDocument && $projectRef}
|
||||
<Scroller>
|
||||
<div class="p-5 pt-6 w-full text-md bottom-divider">
|
||||
<DocumentInfo label={documentsRes.string.ID}>
|
||||
<DocumentPresenter
|
||||
value={$controlledDocument}
|
||||
isRegular
|
||||
disableLink
|
||||
editable={isDocCodeEditable}
|
||||
on:edit={(e) => {
|
||||
handleCodeEdit(e.detail)
|
||||
}}
|
||||
/>
|
||||
</DocumentInfo>
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Category}>
|
||||
<CategoryPresenter value={$controlledDocument.category} />
|
||||
</DocumentInfo>
|
||||
|
||||
{#if !isTemplate}
|
||||
<DocumentInfo label={documentsRes.string.TemplateName}>
|
||||
<DocumentTitlePresenter value={$controlledDocument.template} />
|
||||
</DocumentInfo>
|
||||
{/if}
|
||||
|
||||
{#if isTemplate}
|
||||
<DocumentInfo label={documentsRes.string.DocumentPrefix}>
|
||||
<DocumentPrefixPresenter value={asTemplate} editable={isDocPrefixEditable} />
|
||||
</DocumentInfo>
|
||||
{/if}
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Version}>
|
||||
<DocumentVersionPresenter value={$controlledDocument} />
|
||||
</DocumentInfo>
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Modified}>
|
||||
<DatePresenter
|
||||
value={$controlledDocument.modifiedOn}
|
||||
editable={false}
|
||||
showIcon={false}
|
||||
mode={DateRangeMode.DATETIME}
|
||||
kind="regular"
|
||||
/>
|
||||
</DocumentInfo>
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Status}>
|
||||
<StatePresenter value={$controlledDocument} showTag={false} />
|
||||
</DocumentInfo>
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Owner}>
|
||||
<OwnerPresenter
|
||||
_id={$controlledDocument.owner}
|
||||
object={$controlledDocument}
|
||||
isEditable={$isEditable}
|
||||
value={undefined}
|
||||
shouldShowLabel
|
||||
/>
|
||||
</DocumentInfo>
|
||||
|
||||
<DocumentInfo label={documentsRes.string.Author}>
|
||||
<PersonPresenter value={$controlledDocument.author} disabled={true} />
|
||||
</DocumentInfo>
|
||||
</div>
|
||||
|
||||
<div class="flex-gap-2 p-5 pt-6 w-full text-md">
|
||||
<div class="py-2">
|
||||
<DocumentInfoLabel label={documentsRes.string.MetaAbstract} />
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<AbstractEditor value={$controlledDocument} readonly={!$isEditable} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $controlledDocument.space !== documents.space.UnsortedTemplates}
|
||||
<div class="flex-gap-2 p-5 pt-6 w-full text-md top-divider">
|
||||
<div class="py-2">
|
||||
<DocumentInfoLabel label={documentsRes.string.DocumentInHierarchy} />
|
||||
</div>
|
||||
|
||||
<DocumentFlatHierarchy document={$controlledDocument} project={$projectRef} />
|
||||
</div>
|
||||
{/if}
|
||||
</Scroller>
|
||||
{/if}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { RightPanelTab, $activeRightPanelTab as activeRightPanelTab } from '../../../stores/editors/document'
|
||||
import DocumentInfoTab from './DocumentInfoTab.svelte'
|
||||
import DocumentCommentsTab from './DocumentCommentsTab.svelte'
|
||||
import DocumentApprovalsTab from './DocumentApprovalsTab.svelte'
|
||||
</script>
|
||||
|
||||
{#if $activeRightPanelTab === RightPanelTab.INFO}
|
||||
<DocumentInfoTab />
|
||||
{:else if $activeRightPanelTab === RightPanelTab.COMMENT}
|
||||
<DocumentCommentsTab />
|
||||
{:else if $activeRightPanelTab === RightPanelTab.APPROVALS}
|
||||
<DocumentApprovalsTab />
|
||||
{/if}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Button, IconClose } from '@hcengineering/ui'
|
||||
import { rightPanelTabChanged } from '../../../stores/editors/document'
|
||||
</script>
|
||||
|
||||
<div class="header h-12 flex-between min-h-12 pl-4 pr-4 font-medium text-md bottom-divider">
|
||||
<slot />
|
||||
<Button
|
||||
kind="ghost"
|
||||
icon={IconClose}
|
||||
on:click={() => {
|
||||
rightPanelTabChanged(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { type Ref, SortingOrder, getCurrentAccount } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { type PersonAccount } from '@hcengineering/contact'
|
||||
import documents, {
|
||||
type ControlledDocument,
|
||||
type DocumentMeta,
|
||||
type HierarchyDocument,
|
||||
type Project,
|
||||
type ProjectMeta
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import { compareDocs, notEmpty } from '../../../../utils'
|
||||
import DocumentFlatTreeElement from './DocumentFlatTreeElement.svelte'
|
||||
|
||||
export let document: HierarchyDocument | undefined
|
||||
export let project: Ref<Project>
|
||||
|
||||
const currentUser = getCurrentAccount() as PersonAccount
|
||||
const currentPerson = currentUser.person
|
||||
|
||||
let meta: ProjectMeta | undefined
|
||||
const projectMetaQuery = createQuery()
|
||||
$: if (document !== undefined && project !== undefined) {
|
||||
projectMetaQuery.query(
|
||||
documents.class.ProjectMeta,
|
||||
{
|
||||
project,
|
||||
meta: document.attachedTo
|
||||
},
|
||||
(result) => {
|
||||
;[meta] = result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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]> = []
|
||||
$: {
|
||||
levels = []
|
||||
|
||||
if (parentDocs?.length > 0) {
|
||||
levels.push([parentDocs, false])
|
||||
}
|
||||
|
||||
if (document !== undefined) {
|
||||
levels.push([[document], true])
|
||||
}
|
||||
|
||||
if (directChildrenDocs?.length > 0) {
|
||||
levels.push([directChildrenDocs, false])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if levels.length > 0}
|
||||
{@const [firstDocs, firstHltd] = levels[0]}
|
||||
<div class="root">
|
||||
{#each firstDocs as doc}
|
||||
<DocumentFlatTreeElement {doc} {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}
|
||||
{#if levels.length > 2}
|
||||
{@const [thirdDocs, thirdHltd] = levels[2]}
|
||||
<div class="container">
|
||||
{#each thirdDocs as doc}
|
||||
<DocumentFlatTreeElement {doc} {project} highlighted={thirdHltd} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 1rem;
|
||||
border-left: 2px solid var(--theme-navpanel-border);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import documents, { getDocumentName, type Document, 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 project: Ref<Project>
|
||||
export let highlighted: boolean = false
|
||||
|
||||
const icon = documents.icon.Document
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="antiNav-element root"
|
||||
on:click={() => {
|
||||
const loc = getProjectDocumentLink(doc, project)
|
||||
navigate(loc)
|
||||
}}
|
||||
>
|
||||
{#if icon}
|
||||
<div class="an-element__icon">
|
||||
<Icon
|
||||
{icon}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="an-element__label" class:font-medium={highlighted}>
|
||||
{getDocumentName(doc)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import DocumentInfoLabel from './DocumentInfoLabel.svelte'
|
||||
|
||||
export let label: IntlString
|
||||
</script>
|
||||
|
||||
<div class="flex flex-gap-1 min-h-8 items-center w-full">
|
||||
<div class="label">
|
||||
<DocumentInfoLabel {label} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label {
|
||||
flex-basis: 38%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.field {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
|
||||
export let label: IntlString
|
||||
</script>
|
||||
|
||||
<div class="label">
|
||||
<Label {label} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label {
|
||||
color: var(--theme-dark-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const scrollIntoSection = writable<number | undefined>()
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum LogType {
|
||||
CREATE_DRAFT,
|
||||
DELETE_DRAFT,
|
||||
CHANGE_STATE,
|
||||
CHANGE_OWNER
|
||||
}
|
||||
Reference in New Issue
Block a user