mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 20:57:45 +02:00
Rework documents (#2347)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -16,9 +16,9 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { EmployeeAccount } from '@hcengineering/contact'
|
||||
import { Data, generateId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Document } from '@hcengineering/document'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { Data, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { CollaboratorDocument, Document, DocumentVersionState } from '@hcengineering/document'
|
||||
import { Card, getClient, UserBoxList } from '@hcengineering/presentation'
|
||||
import { Button, createFocusManager, EditBox, FocusHandler } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import document from '../plugin'
|
||||
@@ -32,21 +32,62 @@
|
||||
|
||||
const object: Data<Document> = {
|
||||
name: '',
|
||||
content: 0,
|
||||
editSequence: 0,
|
||||
versions: 0,
|
||||
attachments: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
versionCounter: 0,
|
||||
responsible: [currentUser.employee]
|
||||
version: 0,
|
||||
latest: 0.1,
|
||||
approvers: [],
|
||||
authors: [currentUser.employee],
|
||||
reviewers: [],
|
||||
requests: 0
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
async function createDocument () {
|
||||
await client.createDoc(document.class.Document, document.space.Documents, object, id)
|
||||
const docId = await client.createDoc(document.class.Document, document.space.Documents, object, id)
|
||||
const contentAttachmentId: Ref<CollaboratorDocument> = generateId()
|
||||
const versionId = await client.addCollection(
|
||||
document.class.DocumentVersion,
|
||||
document.space.Documents,
|
||||
docId,
|
||||
document.class.Document,
|
||||
'versions',
|
||||
{
|
||||
content: '',
|
||||
description: '',
|
||||
impact: '',
|
||||
reason: '',
|
||||
state: DocumentVersionState.Draft,
|
||||
version: 0.1,
|
||||
attachments: 0,
|
||||
comments: 0,
|
||||
contentAttachmentId,
|
||||
initialContentId: '' as Ref<CollaboratorDocument>
|
||||
}
|
||||
)
|
||||
|
||||
await client.addCollection(
|
||||
document.class.CollaboratorDocument,
|
||||
document.space.Documents,
|
||||
versionId,
|
||||
document.class.DocumentVersion,
|
||||
'attachments',
|
||||
{
|
||||
file: contentAttachmentId,
|
||||
name: 'content',
|
||||
size: 0,
|
||||
type: 'application/ydoc',
|
||||
description: '',
|
||||
pinned: false,
|
||||
lastModified: Date.now()
|
||||
},
|
||||
contentAttachmentId
|
||||
)
|
||||
|
||||
dispatch('close', id)
|
||||
}
|
||||
|
||||
@@ -75,4 +116,39 @@
|
||||
focusIndex={1}
|
||||
/>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
<UserBoxList
|
||||
items={object.authors}
|
||||
size="small"
|
||||
label={document.string.Authors}
|
||||
emptyLabel={document.string.Authors}
|
||||
kind="no-border"
|
||||
width={'min-content'}
|
||||
on:update={({ detail }) => (object.authors = detail)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
<UserBoxList
|
||||
items={object.approvers}
|
||||
size="small"
|
||||
label={document.string.Approvers}
|
||||
emptyLabel={document.string.Approvers}
|
||||
kind="no-border"
|
||||
width={'min-content'}
|
||||
on:update={({ detail }) => (object.approvers = detail)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap" style:gap={'0.2vw'}>
|
||||
<UserBoxList
|
||||
items={object.reviewers}
|
||||
size="small"
|
||||
label={document.string.Reviewers}
|
||||
emptyLabel={document.string.Reviewers}
|
||||
kind="no-border"
|
||||
width={'min-content'}
|
||||
on:update={({ detail }) => (object.reviewers = detail)}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2022 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 { AttachedData, generateId, Ref } from '@hcengineering/core'
|
||||
import { Document, DocumentVersion, RichDocumentContent } from '@hcengineering/document'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { createFocusManager, FocusHandler, Icon, Label } from '@hcengineering/ui'
|
||||
import Scroller from '@hcengineering/ui/src/components/Scroller.svelte'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { Step } from 'prosemirror-transform'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import document from '../plugin'
|
||||
import { UniqId } from '../uniqId'
|
||||
|
||||
// export let objectId: Ref<Doc>
|
||||
// export let space: Ref<Space>
|
||||
// export let _class: Ref<Class<Doc>>
|
||||
|
||||
export let object: Document
|
||||
// export let content: string
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
const id: Ref<DocumentVersion> = generateId()
|
||||
|
||||
const versionObject: AttachedData<DocumentVersion> = {
|
||||
version: 0,
|
||||
sequenceNumber: object.editSequence,
|
||||
content: '',
|
||||
approved: null
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
async function createDocumentVersion () {
|
||||
versionObject.content = editor.getHTML()
|
||||
|
||||
const incResult = await client.update(
|
||||
object,
|
||||
{
|
||||
$inc: { versionCounter: 1 }
|
||||
},
|
||||
true
|
||||
)
|
||||
versionObject.version = ((incResult as any).object as Document).versionCounter
|
||||
|
||||
await client.addCollection(
|
||||
document.class.DocumentVersion,
|
||||
document.space.Documents,
|
||||
object._id,
|
||||
object._class,
|
||||
'versions',
|
||||
versionObject,
|
||||
id
|
||||
)
|
||||
dispatch('close', id)
|
||||
}
|
||||
|
||||
let lastVersion: DocumentVersion | undefined
|
||||
let richContent: RichDocumentContent[] = []
|
||||
|
||||
function recreateDocument (version?: DocumentVersion): void {
|
||||
const maxVersion = richContent.reduce((a, b) => Math.max(a, b.version), 0)
|
||||
|
||||
const toApply = richContent.findIndex((it) => it.version === (version?.sequenceNumber ?? maxVersion))
|
||||
// Filter steps if some next ones contain them in ignored.
|
||||
|
||||
const asteps: Step[] = []
|
||||
// const newSteps: Step[] = []
|
||||
for (const a of richContent.slice(0, toApply)) {
|
||||
asteps.push(...a.steps.map((it) => Step.fromJSON(editor.schema, it)))
|
||||
}
|
||||
for (const a of richContent.slice(toApply)) {
|
||||
asteps.push(...a.steps.map((it) => Step.fromJSON(editor.schema, it)))
|
||||
}
|
||||
const tr = editor.state.tr
|
||||
|
||||
for (let i = 0; i < asteps.length; i++) {
|
||||
try {
|
||||
tr.step(asteps[i])
|
||||
} catch (err: any) {
|
||||
// debugger
|
||||
console.trace(err)
|
||||
}
|
||||
}
|
||||
if (tr.docChanged) {
|
||||
tr.setMeta('addToHistory', false)
|
||||
const newState = editor.state.apply(tr)
|
||||
editor.view.updateState(newState)
|
||||
}
|
||||
}
|
||||
|
||||
async function update (): Promise<void> {
|
||||
lastVersion = await client.findOne(document.class.DocumentVersion, {}, { sort: { version: -1 } })
|
||||
richContent = await client.findAll(document.class.RichDocumentContent, { attachedTo: object._id })
|
||||
recreateDocument(lastVersion)
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
let editor: Editor
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content: '',
|
||||
editable: false,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
Link.configure({ openOnClick: false }),
|
||||
// ...(supportSubmit ? [Handle] : []), // order important
|
||||
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
|
||||
Placeholder.configure({ placeholder: '' }),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
HTMLAttributes: {
|
||||
class: 'flex flex-grow gap-1 checkbox_style'
|
||||
}
|
||||
}),
|
||||
UniqId
|
||||
// ...extensions
|
||||
],
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
},
|
||||
onBlur: ({ event }) => {},
|
||||
onFocus: () => {},
|
||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {},
|
||||
onSelectionUpdate: () => {}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
const manager = createFocusManager()
|
||||
let element: HTMLElement
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<Card
|
||||
label={document.string.CreateDocumentVersion}
|
||||
okAction={createDocumentVersion}
|
||||
canSave={true}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="flex-row clear-mins">
|
||||
<div class="mr-3 flex-col">
|
||||
<div class="flex-row-center">
|
||||
<div class="p-1">
|
||||
<Icon icon={document.icon.Document} size={'medium'} />
|
||||
</div>
|
||||
{#if lastVersion === undefined}
|
||||
<Label label={document.string.NoVersions} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
<Label label={document.string.Revision} />
|
||||
{object.editSequence}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex-row flex-grow">
|
||||
<div class="h-75p background-bg-focused border-radius-1 p-2">
|
||||
<Scroller>
|
||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -15,345 +15,27 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { generateId, Ref } from '@hcengineering/core'
|
||||
import { Document, RichDocumentContent } from '@hcengineering/document'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { DocumentVersion } from '@hcengineering/document'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import { Editor, HTMLContent } from '@tiptap/core'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Link from '@tiptap/extension-link'
|
||||
// import Typography from '@tiptap/extension-typography'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
// import Collab from '@tiptap/extension-collaboration'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { Step, Transform } from 'prosemirror-transform'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import login from '@hcengineering/login'
|
||||
import document from '../plugin'
|
||||
import { UniqId } from '../uniqId'
|
||||
|
||||
export let object: Document
|
||||
export let defaultTimeout = 1500
|
||||
import { CollaboratorEditor } from '@hcengineering/text-editor'
|
||||
|
||||
const client = getClient()
|
||||
export let object: DocumentVersion
|
||||
export let readonly = false
|
||||
export let initialContentId: string | undefined = undefined
|
||||
|
||||
interface Rebaseable {
|
||||
step: Step
|
||||
inverted: Step
|
||||
origin: Transform
|
||||
}
|
||||
|
||||
const contentQuery = createQuery()
|
||||
|
||||
let richContent: RichDocumentContent[] = []
|
||||
let appliedSequence: number = 0
|
||||
|
||||
let applyTimer: number | undefined = undefined
|
||||
|
||||
// Uncommited steps.
|
||||
let steps: Rebaseable[] = []
|
||||
|
||||
$: contentQuery.query(
|
||||
document.class.RichDocumentContent,
|
||||
{ attachedTo: object._id, collection: 'content' },
|
||||
(res) => {
|
||||
richContent = res
|
||||
},
|
||||
{ sort: { version: 1 } }
|
||||
)
|
||||
|
||||
let content: string = ''
|
||||
export let placeholder: IntlString = document.string.EditorPlaceholder
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
|
||||
let placeHolderStr: string = ''
|
||||
|
||||
const ownRichContent: Ref<RichDocumentContent>[] = [generateId()]
|
||||
|
||||
$: ph = translate(placeholder, {}).then((r) => {
|
||||
placeHolderStr = r
|
||||
})
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export function submit (): void {
|
||||
content = editor.getHTML()
|
||||
dispatch('content', content)
|
||||
}
|
||||
|
||||
export function clear (): void {
|
||||
content = ''
|
||||
editor.commands.clearContent(false)
|
||||
}
|
||||
export function insertText (text: string): void {
|
||||
editor.commands.insertContent(text as HTMLContent)
|
||||
}
|
||||
|
||||
export function toggleBold () {
|
||||
editor.commands.toggleBold()
|
||||
}
|
||||
export function toggleItalic () {
|
||||
editor.commands.toggleItalic()
|
||||
}
|
||||
export function toggleStrike () {
|
||||
editor.commands.toggleStrike()
|
||||
}
|
||||
export function getLink () {
|
||||
return editor.getAttributes('link').href
|
||||
}
|
||||
export function unsetLink () {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
}
|
||||
export function setLink (link: string) {
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: link }).run()
|
||||
}
|
||||
export function checkIsSelectionEmpty () {
|
||||
return editor.view.state.selection.empty
|
||||
}
|
||||
export function toggleOrderedList () {
|
||||
editor.commands.toggleOrderedList()
|
||||
}
|
||||
export function toggleBulletList () {
|
||||
editor.commands.toggleBulletList()
|
||||
}
|
||||
export function toggleBlockquote () {
|
||||
editor.commands.toggleBlockquote()
|
||||
}
|
||||
export function toggleCode () {
|
||||
editor.commands.toggleCode()
|
||||
}
|
||||
export function toggleCodeBlock () {
|
||||
editor.commands.toggleCodeBlock()
|
||||
}
|
||||
let needFocus = false
|
||||
|
||||
let focused = false
|
||||
export function focus (): void {
|
||||
needFocus = true
|
||||
}
|
||||
|
||||
$: if (editor && needFocus) {
|
||||
if (!focused) {
|
||||
editor.commands.focus()
|
||||
}
|
||||
needFocus = false
|
||||
}
|
||||
|
||||
function unconfirmedFrom (transform: Transform): Rebaseable[] {
|
||||
const result = []
|
||||
for (let i = 0; i < transform.steps.length; i++) {
|
||||
result.push({
|
||||
step: transform.steps[i],
|
||||
inverted: transform.steps[i].invert(transform.docs[i]),
|
||||
origin: transform
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function rebaseSteps (steps: readonly Rebaseable[], over: readonly Step[], transform: Transform): Rebaseable[] {
|
||||
for (let i = steps.length - 1; i >= 0; i--) transform.step(steps[i].inverted)
|
||||
for (let i = 0; i < over.length; i++) transform.step(over[i])
|
||||
const result = []
|
||||
for (let i = 0, mapFrom = steps.length; i < steps.length; i++) {
|
||||
const mapped = steps[i].step.map(transform.mapping.slice(mapFrom))
|
||||
mapFrom--
|
||||
if (mapped && !transform.maybeStep(mapped).failed) {
|
||||
;(transform.mapping as any).setMirror(mapFrom, transform.steps.length - 1)
|
||||
result.push({
|
||||
step: mapped,
|
||||
inverted: mapped.invert(transform.docs[transform.docs.length - 1]),
|
||||
origin: steps[i].origin
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function applyEditorSteps (editor: Editor, currentSequence: number, richContent: RichDocumentContent[]): void {
|
||||
const maxVersion = richContent.reduce((a, b) => Math.max(a, b.version), 0)
|
||||
|
||||
const toApply = richContent.filter((it) => it.version > currentSequence && !ownRichContent.includes(it._id))
|
||||
// Filter steps if some next ones contain them in ignored.
|
||||
|
||||
const asteps: Step[] = []
|
||||
for (const a of toApply) {
|
||||
asteps.push(...a.steps.map((it) => Step.fromJSON(editor.schema, it)))
|
||||
}
|
||||
const tr = editor.state.tr
|
||||
|
||||
if (steps.length > 0) {
|
||||
steps = rebaseSteps(steps, asteps, tr)
|
||||
} else {
|
||||
for (let i = 0; i < asteps.length; i++) {
|
||||
try {
|
||||
tr.step(asteps[i])
|
||||
} catch (err: any) {
|
||||
// debugger
|
||||
console.trace(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tr.docChanged) {
|
||||
tr.setMeta('addToHistory', false)
|
||||
const newState = editor.state.apply(tr)
|
||||
editor.view.updateState(newState)
|
||||
appliedSequence = maxVersion
|
||||
content = editor.getHTML()
|
||||
}
|
||||
}
|
||||
|
||||
$: if (editor !== undefined) {
|
||||
// Apply all transactions to editor
|
||||
applyEditorSteps(editor, appliedSequence, richContent)
|
||||
}
|
||||
|
||||
export async function applySteps (): Promise<void> {
|
||||
const seq = object.editSequence
|
||||
if (seq === appliedSequence) {
|
||||
const ops = client.apply(object._class + object._id).match<Document>(document.class.Document, {
|
||||
editSequence: object.editSequence
|
||||
})
|
||||
await ops.update(object, { editSequence: seq + 1 })
|
||||
const sendSteps = steps.length
|
||||
if (sendSteps === 0) {
|
||||
return
|
||||
}
|
||||
const toSend: Step[] = []
|
||||
let c: Step | null = null
|
||||
const rawSteps = steps.slice(0, sendSteps).map((it) => it.step)
|
||||
for (const s of rawSteps) {
|
||||
if (toSend.length === 0) {
|
||||
toSend.push(s)
|
||||
} else if ((c = toSend[toSend.length - 1].merge(s)) != null) {
|
||||
toSend[toSend.length - 1] = c
|
||||
} else {
|
||||
toSend.push(s)
|
||||
}
|
||||
}
|
||||
await ops.addCollection(
|
||||
document.class.RichDocumentContent,
|
||||
object.space,
|
||||
object._id,
|
||||
object._class,
|
||||
'content',
|
||||
{
|
||||
version: seq + 1,
|
||||
steps: toSend.map((it) => it.toJSON())
|
||||
},
|
||||
ownRichContent[ownRichContent.length - 1]
|
||||
)
|
||||
|
||||
if (await ops.commit()) {
|
||||
steps.splice(0, sendSteps)
|
||||
ownRichContent.push(generateId())
|
||||
return
|
||||
}
|
||||
}
|
||||
// retry in case of failure
|
||||
clearTimeout(applyTimer)
|
||||
applyTimer = setTimeout(applySteps, 100 + Math.random() * 500)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
ph.then(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content: content,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
Link.configure({ openOnClick: false }),
|
||||
// ...(supportSubmit ? [Handle] : []), // order important
|
||||
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
|
||||
Placeholder.configure({ placeholder: placeHolderStr }),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
HTMLAttributes: {
|
||||
class: 'flex flex-grow gap-1 checkbox_style'
|
||||
}
|
||||
}),
|
||||
UniqId
|
||||
// ...extensions
|
||||
],
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
},
|
||||
onBlur: ({ event }) => {
|
||||
focused = false
|
||||
dispatch('blur', event)
|
||||
applySteps()
|
||||
},
|
||||
onFocus: () => {
|
||||
focused = true
|
||||
dispatch('focus', editor.getHTML())
|
||||
},
|
||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
||||
if (op.transaction.docChanged) {
|
||||
steps = steps.concat(unconfirmedFrom(op.transaction))
|
||||
|
||||
clearTimeout(applyTimer)
|
||||
applyTimer = setTimeout(applySteps, defaultTimeout)
|
||||
}
|
||||
|
||||
content = editor.getHTML()
|
||||
},
|
||||
onSelectionUpdate: () => dispatch('selection-update')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
applySteps()
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
const token = getMetadata(login.metadata.LoginToken) ?? ''
|
||||
const collaboratorURL = getMetadata(document.metadata.CollaboratorUrl) ?? ''
|
||||
</script>
|
||||
|
||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||
|
||||
<style lang="scss" global>
|
||||
.ProseMirror {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
outline: none;
|
||||
line-height: 150%;
|
||||
color: var(--accent-color);
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
/* Placeholder (at the top) */
|
||||
p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--dark-color);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--theme-bg-accent-hover);
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: var(--theme-bg-accent-hover);
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<CollaboratorEditor
|
||||
documentId={object.contentAttachmentId}
|
||||
{token}
|
||||
{collaboratorURL}
|
||||
{readonly}
|
||||
on:content
|
||||
{initialContentId}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,6 @@
|
||||
<div class="icon">
|
||||
<Icon icon={document.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label">{value.name}</span>
|
||||
<span class="label">{value.name}-{value.version}</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -15,12 +15,24 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { DocumentVersion } from '@hcengineering/document'
|
||||
import { getPanelURI, Icon, Label } from '@hcengineering/ui'
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Document, DocumentVersion } from '@hcengineering/document'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { getPanelURI, Icon } from '@hcengineering/ui'
|
||||
import document from '../plugin'
|
||||
|
||||
export let value: DocumentVersion
|
||||
export let value: WithLookup<DocumentVersion>
|
||||
export let inline = false
|
||||
|
||||
$: doc = value.$lookup?.attachedTo as Document
|
||||
|
||||
$: if (doc === undefined) {
|
||||
getClient()
|
||||
.findOne(document.class.Document, { _id: value.attachedTo as Ref<Document> })
|
||||
.then((res) => {
|
||||
doc = res as Document
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
@@ -33,8 +45,7 @@
|
||||
<Icon icon={document.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="label">
|
||||
<Label label={document.string.Version} />
|
||||
{value.version}
|
||||
{doc?.name} - {value.version}
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2022 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/document'
|
||||
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import { Table } from '@hcengineering/view-resources'
|
||||
import document from '../plugin'
|
||||
import CreateDocumentVersion from './CreateDocumentVersion.svelte'
|
||||
|
||||
export let object: Document
|
||||
|
||||
const createApp = (ev: MouseEvent): void => {
|
||||
showPopup(CreateDocumentVersion, { object }, 'top')
|
||||
}
|
||||
const config: (BuildModelKey | string)[] = ['version', 'sequenceNumber', 'approved']
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
<div class="antiSection-header">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={document.icon.Document} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title">
|
||||
<Label label={document.string.Versions} />
|
||||
</span>
|
||||
<Button id="appls.add" icon={IconAdd} kind={'transparent'} shape={'circle'} on:click={createApp} />
|
||||
</div>
|
||||
{#if object.versions > 0}
|
||||
<Table
|
||||
_class={document.class.DocumentVersion}
|
||||
{config}
|
||||
query={{ attachedTo: object._id }}
|
||||
loadingProps={{ length: object.versions }}
|
||||
/>
|
||||
{:else}
|
||||
<div class="antiSection-empty solid flex-col-center mt-3">
|
||||
<span class="dark-color">
|
||||
<Label label={document.string.NoVersions} />
|
||||
</span>
|
||||
<span class="over-underline content-accent-color" on:click={createApp}>
|
||||
<Label label={document.string.CreateAnVersion} />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,159 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2022 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, RichDocumentContent } from '@hcengineering/document'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Editor } from '@tiptap/core'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Placeholder from '@tiptap/extension-placeholder'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Transaction } from 'prosemirror-state'
|
||||
import { Step } from 'prosemirror-transform'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import document from '../plugin'
|
||||
import { UniqId } from '../uniqId'
|
||||
|
||||
export let object: Document
|
||||
|
||||
export let revision: number
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let richContent: RichDocumentContent[] = []
|
||||
|
||||
function recreateDocument (version: number): void {
|
||||
const toApply = richContent.filter((it) => it.version <= version)
|
||||
// Filter steps if some next ones contain them in ignored.
|
||||
|
||||
const asteps: Step[] = []
|
||||
for (const a of toApply) {
|
||||
asteps.push(...a.steps.map((it) => Step.fromJSON(editor.schema, it)))
|
||||
}
|
||||
|
||||
const tr = editor.state.tr.delete(0, editor.state.doc.content.size)
|
||||
|
||||
for (let i = 0; i < asteps.length; i++) {
|
||||
try {
|
||||
tr.step(asteps[i])
|
||||
} catch (err: any) {
|
||||
// debugger
|
||||
console.trace(err)
|
||||
}
|
||||
}
|
||||
if (tr.docChanged) {
|
||||
tr.setMeta('addToHistory', false)
|
||||
const newState = editor.state.apply(tr)
|
||||
editor.view.updateState(newState)
|
||||
}
|
||||
}
|
||||
|
||||
async function update (revision: number): Promise<void> {
|
||||
richContent = await client.findAll(document.class.RichDocumentContent, { attachedTo: object._id })
|
||||
recreateDocument(revision)
|
||||
}
|
||||
|
||||
$: update(revision)
|
||||
|
||||
let editor: Editor
|
||||
|
||||
onMount(() => {
|
||||
editor = new Editor({
|
||||
element,
|
||||
content: '',
|
||||
editable: false,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
Link.configure({ openOnClick: false }),
|
||||
// ...(supportSubmit ? [Handle] : []), // order important
|
||||
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
|
||||
Placeholder.configure({ placeholder: '' }),
|
||||
TaskList,
|
||||
TaskItem.configure({
|
||||
nested: true,
|
||||
HTMLAttributes: {
|
||||
class: 'flex flex-grow gap-1 checkbox_style'
|
||||
}
|
||||
}),
|
||||
UniqId
|
||||
// ...extensions
|
||||
],
|
||||
onTransaction: () => {
|
||||
// force re-render so `editor.isActive` works as expected
|
||||
editor = editor
|
||||
},
|
||||
onBlur: ({ event }) => {},
|
||||
onFocus: () => {},
|
||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {},
|
||||
onSelectionUpdate: () => {}
|
||||
})
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (editor) {
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
let element: HTMLElement
|
||||
</script>
|
||||
|
||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||
|
||||
<style lang="scss" global>
|
||||
.ProseMirror {
|
||||
overflow-y: auto;
|
||||
max-height: 60vh;
|
||||
outline: none;
|
||||
line-height: 150%;
|
||||
color: var(--accent-color);
|
||||
|
||||
p:not(:last-child) {
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
/* Placeholder (at the top) */
|
||||
p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--dark-color);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--theme-bg-accent-hover);
|
||||
}
|
||||
&::-webkit-scrollbar-corner {
|
||||
background-color: var(--theme-bg-accent-hover);
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,12 +16,20 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attachments } from '@hcengineering/attachment-resources'
|
||||
import core, { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Document, DocumentVersion } from '@hcengineering/document'
|
||||
import { EmployeeAccount } from '@hcengineering/contact'
|
||||
import core, { Class, Doc, generateId, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
|
||||
import {
|
||||
CollaboratorDocument,
|
||||
Document,
|
||||
DocumentRequest,
|
||||
DocumentRequestKind,
|
||||
DocumentVersion,
|
||||
DocumentVersionState
|
||||
} from '@hcengineering/document'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import tags from '@hcengineering/tags'
|
||||
|
||||
import {
|
||||
@@ -29,9 +37,11 @@
|
||||
Component,
|
||||
EditBox,
|
||||
eventToHTMLElement,
|
||||
IconAdd,
|
||||
IconCheck,
|
||||
IconClose,
|
||||
IconEdit,
|
||||
IconMoreH,
|
||||
IconShare,
|
||||
Label,
|
||||
SelectPopup,
|
||||
showPopup
|
||||
@@ -40,9 +50,7 @@
|
||||
import ClassAttributeBar from '@hcengineering/view-resources/src/components/ClassAttributeBar.svelte'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import document from '../plugin'
|
||||
import CreateDocumentVersion from './CreateDocumentVersion.svelte'
|
||||
import DocumentEditor from './DocumentEditor.svelte'
|
||||
import DocumentViewer from './DocumentViewer.svelte'
|
||||
|
||||
// import ControlPanel from './ControlPanel.svelte'
|
||||
// import CopyToClipboard from './CopyToClipboard.svelte'
|
||||
@@ -63,7 +71,6 @@
|
||||
let name = ''
|
||||
|
||||
let innerWidth: number
|
||||
let isEditing = false
|
||||
|
||||
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
|
||||
|
||||
@@ -78,10 +85,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = getCurrentAccount() as EmployeeAccount
|
||||
|
||||
onDestroy(async () => {
|
||||
notificationClient.then((client) => client.updateLastView(_id, _class))
|
||||
})
|
||||
|
||||
let requests: DocumentRequest[] = []
|
||||
|
||||
$: myRequests = requests.filter((it) => it.assignee === currentUser.employee)
|
||||
|
||||
$: approveRequest = myRequests.find((it) => it.kind === DocumentRequestKind.Approve)
|
||||
$: allApproveRequest = requests.find((it) => it.kind === DocumentRequestKind.Approve)
|
||||
// $: changesRequest = myRequests.find((it) => it.kind === DocumentRequestKind.Changes)
|
||||
// $: reviewRequest = myRequests.find((it) => it.kind === DocumentRequestKind.Review)
|
||||
|
||||
$: _id &&
|
||||
_class &&
|
||||
query.query(
|
||||
@@ -89,31 +107,22 @@
|
||||
{ _id },
|
||||
async (result) => {
|
||||
;[documentObject] = result
|
||||
name = documentObject.name
|
||||
name = documentObject?.name ?? ''
|
||||
|
||||
requests = (documentObject.$lookup?.requests as DocumentRequest[]) ?? []
|
||||
},
|
||||
{}
|
||||
{
|
||||
lookup: {
|
||||
_id: {
|
||||
requests: document.class.DocumentRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: canSave = name.trim().length > 0
|
||||
|
||||
function edit (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
isEditing = true
|
||||
}
|
||||
|
||||
let documentEditor: DocumentEditor
|
||||
|
||||
function commitEditing (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
documentEditor.applySteps().then(() => {
|
||||
autoSelect = true
|
||||
isEditing = false
|
||||
})
|
||||
}
|
||||
|
||||
async function save (ev: Event) {
|
||||
async function saveTitle (ev: Event) {
|
||||
ev.preventDefault()
|
||||
|
||||
if (!documentObject || !canSave) {
|
||||
@@ -134,7 +143,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
|
||||
dispatch('open', { ignoreKeys: ['comments', 'name'] })
|
||||
})
|
||||
|
||||
const versionQuery = createQuery()
|
||||
@@ -145,6 +154,9 @@
|
||||
{ attachedTo: _id },
|
||||
(res) => {
|
||||
versions = res
|
||||
if (autoSelect) {
|
||||
version = versions[versions.length - 1]
|
||||
}
|
||||
},
|
||||
{ sort: { version: 1 } }
|
||||
)
|
||||
@@ -152,19 +164,28 @@
|
||||
|
||||
let info: any
|
||||
|
||||
let labels: Record<DocumentVersionState, string> = {
|
||||
[DocumentVersionState.Draft]: '',
|
||||
[DocumentVersionState.Approved]: '',
|
||||
[DocumentVersionState.Rejected]: ''
|
||||
}
|
||||
|
||||
async function updateLabels (): Promise<void> {
|
||||
labels = {
|
||||
[DocumentVersionState.Draft]: await translate(document.string.Draft, {}),
|
||||
[DocumentVersionState.Approved]: await translate(document.string.Approved, {}),
|
||||
[DocumentVersionState.Rejected]: await translate(document.string.Rejected, {})
|
||||
}
|
||||
}
|
||||
updateLabels()
|
||||
|
||||
$: {
|
||||
const ifo: any = [
|
||||
...versions.map((it) => ({
|
||||
id: it._id as string,
|
||||
text: `${it.version} - ${new Date(it.modifiedOn).toDateString()} `
|
||||
text: `${it.version} - ${labels[it.state]} - ${new Date(it.modifiedOn).toDateString()} `
|
||||
}))
|
||||
]
|
||||
if (lastVersion === undefined || lastVersion.sequenceNumber !== documentObject?.editSequence) {
|
||||
ifo.push({
|
||||
id: '#latest',
|
||||
label: document.string.LastRevision
|
||||
})
|
||||
}
|
||||
info = ifo
|
||||
}
|
||||
function selectVersion (event: MouseEvent): void {
|
||||
@@ -184,16 +205,177 @@
|
||||
)
|
||||
}
|
||||
|
||||
function createVersion (event: MouseEvent) {
|
||||
showPopup(CreateDocumentVersion, { object: documentObject }, 'top')
|
||||
}
|
||||
|
||||
$: lastVersion = versions[versions.length - 1]
|
||||
$: readonly = !documentObject?.authors.includes(currentUser.employee)
|
||||
|
||||
let autoSelect = true
|
||||
$: if (autoSelect && version === undefined && lastVersion) {
|
||||
version = lastVersion
|
||||
autoSelect = false
|
||||
|
||||
type ModelType = 'view' | 'edit' // | 'suggest'
|
||||
let mode: ModelType = 'view'
|
||||
const modeLabels = {
|
||||
view: document.string.ViewMode,
|
||||
edit: document.string.EditMode
|
||||
// ,suggest: document.string.SuggestMode
|
||||
}
|
||||
|
||||
function selectMode (event: MouseEvent): void {
|
||||
showPopup(
|
||||
SelectPopup,
|
||||
{
|
||||
value: Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
id: mode,
|
||||
label
|
||||
})),
|
||||
placeholder: document.string.Version,
|
||||
searchable: false
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
(res) => {
|
||||
if (res != null) {
|
||||
mode = res
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function doEdit (documentObject: Document): Promise<void> {
|
||||
processing = true
|
||||
// Looking for a draft version
|
||||
const draft = versions.find((it) => it.state === DocumentVersionState.Draft)
|
||||
const lastVersion = versions[versions.length - 1]
|
||||
if (draft === undefined) {
|
||||
// We need to create draft document.
|
||||
const newVersion = Math.round(documentObject.latest * 1000 + 100) / 1000
|
||||
|
||||
const versionId: Ref<DocumentVersion> = generateId()
|
||||
const contentAttachmentId: Ref<CollaboratorDocument> = generateId()
|
||||
|
||||
const ops = client
|
||||
.apply(documentObject._id)
|
||||
.match(document.class.Document, { _id: documentObject._id, latest: documentObject.latest })
|
||||
|
||||
ops.update(documentObject, { latest: newVersion })
|
||||
|
||||
ops.addCollection(
|
||||
document.class.DocumentVersion,
|
||||
documentObject.space,
|
||||
documentObject._id,
|
||||
documentObject._class,
|
||||
'versions',
|
||||
{
|
||||
content: lastVersion.content,
|
||||
description: '',
|
||||
impact: '',
|
||||
reason: '',
|
||||
state: DocumentVersionState.Draft,
|
||||
version: newVersion,
|
||||
attachments: 0,
|
||||
comments: 0,
|
||||
initialContentId: lastVersion.contentAttachmentId,
|
||||
contentAttachmentId
|
||||
},
|
||||
versionId
|
||||
)
|
||||
ops.addCollection(
|
||||
document.class.CollaboratorDocument,
|
||||
documentObject.space,
|
||||
versionId,
|
||||
document.class.DocumentVersion,
|
||||
'attachments',
|
||||
{
|
||||
file: contentAttachmentId,
|
||||
name: 'content',
|
||||
size: 0,
|
||||
type: 'application/ydoc',
|
||||
description: '',
|
||||
pinned: false,
|
||||
lastModified: Date.now()
|
||||
},
|
||||
contentAttachmentId
|
||||
)
|
||||
if (await ops.commit()) {
|
||||
// We should have a draft created by someone else.
|
||||
version = undefined
|
||||
autoSelect = true
|
||||
}
|
||||
}
|
||||
mode = 'edit'
|
||||
processing = false
|
||||
}
|
||||
|
||||
let processing = false
|
||||
|
||||
let content = ''
|
||||
|
||||
const updateRequests = async (kind: DocumentRequestKind): Promise<void> => {
|
||||
processing = true
|
||||
if (documentObject === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const requests = await client.findAll(document.class.DocumentRequest, {
|
||||
attachedTo: documentObject?._id,
|
||||
kind
|
||||
})
|
||||
for (const a of documentObject?.approvers ?? []) {
|
||||
const ex = requests.find((it) => it.assignee === a)
|
||||
if (ex === undefined) {
|
||||
await client.addCollection(
|
||||
document.class.DocumentRequest,
|
||||
documentObject.space,
|
||||
documentObject._id,
|
||||
documentObject._class,
|
||||
'requests',
|
||||
{
|
||||
assignee: a,
|
||||
kind
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (version) {
|
||||
await client.update(version, {
|
||||
content
|
||||
})
|
||||
}
|
||||
|
||||
mode = 'view'
|
||||
|
||||
processing = false
|
||||
}
|
||||
|
||||
const updateState = async (state: DocumentVersionState): Promise<void> => {
|
||||
processing = true
|
||||
if (documentObject === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const draft = versions.find((it) => it.state === DocumentVersionState.Draft)
|
||||
if (draft !== undefined) {
|
||||
// We need to create draft document.
|
||||
const newVersion = documentObject.version + 1
|
||||
|
||||
const ops = client
|
||||
.apply(documentObject._id)
|
||||
.match(document.class.Document, { _id: documentObject._id, latest: documentObject.latest })
|
||||
|
||||
if (state === DocumentVersionState.Approved) {
|
||||
ops.update(documentObject, { latest: newVersion, version: newVersion })
|
||||
ops.update(draft, { version: newVersion, state })
|
||||
} else {
|
||||
ops.update(draft, { state })
|
||||
}
|
||||
// Remove all requests
|
||||
|
||||
if (await ops.commit()) {
|
||||
const docs = await client.findAll(document.class.DocumentRequest, { attachedTo: documentObject._id })
|
||||
for (const d of docs) {
|
||||
client.remove(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processing = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -203,7 +385,6 @@
|
||||
isHeader
|
||||
isAside={true}
|
||||
isSub={false}
|
||||
withoutActivity={isEditing}
|
||||
bind:innerWidth
|
||||
on:close={() => dispatch('close')}
|
||||
>
|
||||
@@ -216,59 +397,105 @@
|
||||
bind:value={name}
|
||||
placeholder={document.string.DocumentNamePlaceholder}
|
||||
kind="large-style"
|
||||
on:blur={(evt) => save(evt)}
|
||||
on:blur={(evt) => saveTitle(evt)}
|
||||
/>
|
||||
|
||||
<div class="p-1">-</div>
|
||||
<Button disabled={isEditing || info.length === 1} kind={'link-bordered'} on:click={selectVersion}>
|
||||
<Button loading={processing} kind={'link-bordered'} on:click={selectVersion} disabled={info.length < 2}>
|
||||
<svelte:fragment slot="content">
|
||||
{#if version && !isEditing}
|
||||
<Label label={document.string.Version} /> {version.version}
|
||||
{#if version}
|
||||
{version.version} - {labels[version.state]}
|
||||
{:else}
|
||||
<Label label={document.string.Revision} /> {documentObject.editSequence}
|
||||
<Label label={document.string.Draft} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
|
||||
{#if !isEditing}
|
||||
{#if documentObject && version && version.sequenceNumber !== documentObject.editSequence}
|
||||
(<Label label={document.string.Revision} /> {version.sequenceNumber} / {documentObject.editSequence})
|
||||
{:else if documentObject}
|
||||
(<Label label={document.string.Revision} /> {documentObject.editSequence})
|
||||
{/if}
|
||||
{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="tools">
|
||||
{#if isEditing}
|
||||
<Button disabled={!canSave} label={presentation.string.Save} on:click={commitEditing} />
|
||||
{:else}
|
||||
<Button icon={IconEdit} kind={'transparent'} size={'medium'} on:click={edit} />
|
||||
{/if}
|
||||
{#if lastVersion === undefined || lastVersion.sequenceNumber !== documentObject.editSequence}
|
||||
{#if version && version?.state !== DocumentVersionState.Draft}
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
loading={processing}
|
||||
kind={'link-bordered'}
|
||||
label={document.string.CreateDraft}
|
||||
on:click={() => {
|
||||
if (documentObject) {
|
||||
doEdit(documentObject)
|
||||
}
|
||||
}}
|
||||
icon={IconEdit}
|
||||
size={'medium'}
|
||||
on:click={createVersion}
|
||||
showTooltip={{ label: document.string.CreateDocumentVersion }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !readonly && version && version?.state === DocumentVersionState.Draft && version.version === documentObject.latest && !allApproveRequest}
|
||||
<Button
|
||||
loading={processing}
|
||||
kind={'link-bordered'}
|
||||
label={document.string.SendForApproval}
|
||||
on:click={() => updateRequests(DocumentRequestKind.Approve)}
|
||||
icon={IconShare}
|
||||
size={'medium'}
|
||||
disabled={documentObject?.approvers?.length === 0}
|
||||
/>
|
||||
<Button
|
||||
loading={processing}
|
||||
kind={'link-bordered'}
|
||||
label={document.string.SendForReview}
|
||||
on:click={() => updateRequests(DocumentRequestKind.Review)}
|
||||
icon={IconShare}
|
||||
size={'medium'}
|
||||
disabled={documentObject?.reviewers?.length === 0}
|
||||
/>
|
||||
{/if}
|
||||
{#if version?.state === DocumentVersionState.Draft && approveRequest}
|
||||
<Button
|
||||
loading={processing}
|
||||
kind={'link-bordered'}
|
||||
label={document.string.Approve}
|
||||
on:click={() => updateState(DocumentVersionState.Approved)}
|
||||
icon={IconCheck}
|
||||
size={'medium'}
|
||||
/>
|
||||
<Button
|
||||
loading={processing}
|
||||
kind={'link-bordered'}
|
||||
label={document.string.Reject}
|
||||
on:click={() => updateState(DocumentVersionState.Rejected)}
|
||||
icon={IconClose}
|
||||
size={'medium'}
|
||||
/>
|
||||
{/if}
|
||||
{#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined}
|
||||
<Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}>
|
||||
<svelte:fragment slot="content">
|
||||
<Label label={modeLabels[mode]} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isEditing}
|
||||
<div class="popupPanel-body__main-content py-10 clear-mins content">
|
||||
<DocumentEditor object={documentObject} bind:this={documentEditor} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="description-preview select-text mt-2">
|
||||
<DocumentViewer object={documentObject} revision={version?.sequenceNumber ?? documentObject.editSequence} />
|
||||
</div>
|
||||
<div class="description-preview select-text mt-2 emphasized">
|
||||
{#if version && version.state === DocumentVersionState.Draft && approveRequest === undefined}
|
||||
{#key version?._id}
|
||||
<DocumentEditor
|
||||
object={version}
|
||||
initialContentId={version.initialContentId}
|
||||
readonly={mode === 'view'}
|
||||
on:content={(evt) => {
|
||||
content = evt.detail
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{:else if version}
|
||||
<MessageViewer message={version.content} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="p-1 mt-6">
|
||||
<Attachments objectId={documentObject._id} space={documentObject.space} _class={documentObject._class} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="p-1 mt-6">
|
||||
<Attachments objectId={documentObject._id} space={documentObject.space} _class={documentObject._class} />
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="custom-attributes">
|
||||
<ClassAttributeBar
|
||||
@@ -276,6 +503,7 @@
|
||||
_class={documentObject._class}
|
||||
to={core.class.Doc}
|
||||
ignoreKeys={['name']}
|
||||
{readonly}
|
||||
/>
|
||||
|
||||
<div class="divider" />
|
||||
@@ -295,12 +523,7 @@
|
||||
<span class="label">
|
||||
<Label label={document.string.LastRevision} />
|
||||
</span>
|
||||
<span>{documentObject?.editSequence}</span>
|
||||
|
||||
<span class="label">
|
||||
<Label label={document.string.Versions} />
|
||||
</span>
|
||||
<span>{documentObject?.versions}</span>
|
||||
<span>{documentObject?.latest}</span>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Panel>
|
||||
@@ -332,4 +555,10 @@
|
||||
.labelTop {
|
||||
align-self: start;
|
||||
}
|
||||
.emphasized {
|
||||
padding: 1rem;
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-hover);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
const currentUser = getCurrentAccount() as EmployeeAccount
|
||||
</script>
|
||||
|
||||
<Documents query={{ responsible: currentUser.employee }} />
|
||||
<Documents query={{ authors: currentUser.employee }} />
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2022 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 { WithLookup } from '@hcengineering/core'
|
||||
import { Document, DocumentVersion } from '@hcengineering/document'
|
||||
|
||||
export let value: WithLookup<Document>
|
||||
|
||||
let lastVersion: DocumentVersion | undefined
|
||||
|
||||
$: if (value.$lookup?.versions !== undefined) {
|
||||
let vv = -1
|
||||
let dv: DocumentVersion | undefined
|
||||
for (const v of value.$lookup.versions as DocumentVersion[]) {
|
||||
if (v.version > vv) {
|
||||
dv = v
|
||||
vv = v.version
|
||||
}
|
||||
}
|
||||
lastVersion = dv
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="flex-row-center">
|
||||
{#if lastVersion}
|
||||
{lastVersion.sequenceNumber}<span class="p-1">/</span>
|
||||
{/if}
|
||||
{value.editSequence}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -16,42 +16,28 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Document, DocumentVersion } from '@hcengineering/document'
|
||||
import { DocumentVersion, DocumentVersionState } from '@hcengineering/document'
|
||||
import { EastSideColor, FernColor, Label, SeaBuckthornColor } from '@hcengineering/ui'
|
||||
import document from '../plugin'
|
||||
|
||||
export let value: WithLookup<Document>
|
||||
|
||||
let lastVersion: DocumentVersion | undefined
|
||||
|
||||
$: if (value.$lookup?.versions !== undefined) {
|
||||
let vv = -1
|
||||
let dv: DocumentVersion | undefined
|
||||
for (const v of value.$lookup.versions as DocumentVersion[]) {
|
||||
if (v.version > vv) {
|
||||
dv = v
|
||||
vv = v.version
|
||||
}
|
||||
}
|
||||
lastVersion = dv
|
||||
}
|
||||
export let value: WithLookup<DocumentVersion>
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<div class="status-container">
|
||||
{#if value.versions === 0}
|
||||
{#if value.state === DocumentVersionState.Draft}
|
||||
<div class="status p-1" style:background-color={EastSideColor}>
|
||||
<Label label={document.string.Draft} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value.editSequence !== lastVersion?.sequenceNumber}
|
||||
{#if value.state === DocumentVersionState.Approved}
|
||||
<div class="status p-1" style:background-color={SeaBuckthornColor}>
|
||||
<Label label={document.string.PendingReview} />
|
||||
<Label label={document.string.Approved} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if value.editSequence === lastVersion?.sequenceNumber}
|
||||
{#if value.state === DocumentVersionState.Rejected}
|
||||
<div class="status p-1" style:background-color={FernColor}>
|
||||
<Label label={document.string.Latest} />
|
||||
<Label label={document.string.Rejected} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user