TSK-343: Remember unfinished comment per document (#2400)

* Add draft for comments

Signed-off-by: Denis Maslennikov <denis.maslennikov@gmail.com>

* Fix missed code

Signed-off-by: Denis Maslennikov <denis.maslennikov@gmail.com>

* Refactoring draft and attachments

Signed-off-by: Denis Maslennikov <denis.maslennikov@gmail.com>

Signed-off-by: Denis Maslennikov <denis.maslennikov@gmail.com>
This commit is contained in:
Denis Maslennikov 2022-12-05 18:08:36 +07:00 committed by GitHub
parent d4d3502a3b
commit 2ed837ac59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 167 additions and 9 deletions

View File

@ -54,6 +54,11 @@
let isSelectionEmpty = true
let isEmpty = true
$: setContent(content)
function setContent (content: string) {
textEditor?.setContent(content)
}
interface RefAction {
label: IntlString
icon: Asset | AnySvelteComponent
@ -286,6 +291,7 @@
}}
extensions={editorExtensions}
on:selection-update={updateFormattingState}
on:update
/>
</div>
{#if showSend}

View File

@ -48,7 +48,13 @@
dispatch('content', content)
}
}
export function setContent (newContent: string): void {
if (content !== newContent) {
content = newContent
editor.commands.setContent(content)
isEmpty = editor.isEmpty
}
}
export function clear (): void {
content = ''
editor.commands.clearContent(false)
@ -181,6 +187,9 @@
Placeholder.configure({ placeholder: placeHolderStr }),
...extensions
],
parseOptions: {
preserveWhitespace: 'full'
},
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor
@ -199,6 +208,9 @@
dispatch('value', content)
dispatch('update', content)
},
onCreate: () => {
isEmpty = editor.isEmpty
},
onSelectionUpdate: () => dispatch('selection-update')
})
})

View File

@ -142,7 +142,7 @@
</Scroller>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
<Component is={chunter.component.CommentInput} props={{ object, shouldUseDraft: true }} />
</div>
{/if}
</div>
@ -164,7 +164,7 @@
</div>
{#if showCommenInput}
<div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} />
<Component is={chunter.component.CommentInput} props={{ object, shouldUseDraft: true }} />
</div>
{/if}
<div class="p-activity select-text" id={activity.string.Activity}>

View File

@ -28,6 +28,7 @@
export let _class: Ref<Class<Doc>>
export let content: string = ''
export let showSend = true
export let shouldUseDraft: boolean = false
export function submit (): void {
refInput.submit()
}
@ -39,6 +40,7 @@
const client = getClient()
const query = createQuery()
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
let originalAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
const newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
@ -79,6 +81,9 @@
})
newAttachments.add(_id)
attachments = attachments
if (shouldUseDraft) {
await createAttachments()
}
} catch (err: any) {
setPlatformStatus(unknownError(err))
}
@ -110,6 +115,9 @@
async function removeAttachment (attachment: Attachment): Promise<void> {
removedAttachments.add(attachment)
attachments.delete(attachment._id)
if (shouldUseDraft) {
await createAttachments()
}
attachments = attachments
}
@ -139,7 +147,7 @@
}
})
async function onMessage (event: CustomEvent) {
export function createAttachments (): Promise<void> {
saved = true
const promises: Promise<any>[] = []
newAttachments.forEach((p) => {
@ -151,10 +159,18 @@
removedAttachments.forEach((p) => {
promises.push(deleteAttachment(p))
})
await Promise.all(promises)
return Promise.all(promises).then()
}
async function onMessage (event: CustomEvent) {
await createAttachments()
dispatch('message', { message: event.detail, attachments: attachments.size })
}
async function onUpdate (event: CustomEvent) {
dispatch('update', { message: event.detail, attachments: attachments.size })
}
function pasteAction (evt: ClipboardEvent): void {
let t: HTMLElement | null = evt.target as HTMLElement
let allowed = false
@ -224,6 +240,7 @@
on:attach={() => {
inputFile.click()
}}
on:update={onUpdate}
/>
</div>
</div>

View File

@ -1,6 +1,6 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// Copyright © 2021, 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
@ -19,12 +19,101 @@
import { getClient } from '@hcengineering/presentation'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { createBacklinks } from '../backlinks'
import { draftStore, updateDraftStore } from '../drafts'
import chunter from '../plugin'
const client = getClient()
export let object: Doc
export let shouldUseDraft: boolean = false
const client = getClient()
const _class = chunter.class.Comment
let _id: Ref<Comment> = generateId()
let inputContent: string = ''
let commentInputBox: AttachmentRefInput
let draftComment: Comment | undefined = undefined
let saveTimer: number | undefined
$: updateDraft(object)
$: updateCommentFromDraft(draftComment)
async function updateDraft (object: Doc) {
if (!shouldUseDraft) {
return
}
draftComment = $draftStore[object._id]
if (!draftComment) {
_id = generateId()
}
}
async function updateCommentFromDraft (draftComment: Comment | undefined) {
if (!shouldUseDraft) {
return
}
inputContent = draftComment ? draftComment.message : ''
_id = draftComment ? draftComment._id : _id
}
function commentIsEmpty (message: string, attachments: number): boolean {
return (message === '<p></p>' || message === '') && !(attachments > 0)
}
async function saveDraft (object: Doc) {
if (draftComment) {
draftComment._id = _id
$draftStore[object._id] = draftComment
} else {
delete $draftStore[object._id]
}
updateDraftStore(object._id, draftComment)
}
async function handleCommentUpdate (message: string, attachments: number) {
if (commentIsEmpty(message, attachments)) {
draftComment = undefined
saveDraft(object)
_id = generateId()
return
}
if (!draftComment) {
draftComment = createDraftFromObject()
}
draftComment.message = message
draftComment.attachments = attachments
await commentInputBox.createAttachments()
saveDraft(object)
}
async function onUpdate (event: CustomEvent) {
if (!shouldUseDraft) {
return
}
const { message, attachments } = event.detail
if (saveTimer) {
clearTimeout(saveTimer)
}
saveTimer = setTimeout(() => {
handleCommentUpdate(message, attachments)
}, 200)
}
function createDraftFromObject () {
const newDraft: Comment = {
_id,
_class: chunter.class.Comment,
space: object.space,
modifiedOn: Date.now(),
modifiedBy: object.modifiedBy,
attachedTo: object._id,
attachedToClass: object._class,
collection: 'comments',
message: '',
attachments: 0
}
return newDraft
}
async function onMessage (event: CustomEvent) {
const { message, attachments } = event.detail
@ -40,8 +129,21 @@
// Create an backlink to document
await createBacklinks(client, object._id, object._class, _id, message)
// Remove draft from Local Storage
_id = generateId()
draftComment = undefined
await saveDraft(object)
}
</script>
<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage} />
<AttachmentRefInput
bind:this={commentInputBox}
bind:content={inputContent}
{_class}
space={object.space}
bind:objectId={_id}
shouldUseDraft
on:message={onMessage}
on:update={onUpdate}
/>

View File

@ -0,0 +1,18 @@
import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui'
import { writable } from 'svelte/store'
import chunter from './plugin'
/**
* @public
*/
// eslint-disable-next-line
export const draftStore = writable<Record<string, any>>(fetchMetadataLocalStorage(chunter.metadata.Draft) || {})
console.log('draft store', draftStore)
export function updateDraftStore (id: string, draft: any): void {
draftStore.update((drafts) => {
drafts[id] = draft
setMetadataLocalStorage(chunter.metadata.Draft, drafts)
return drafts
})
}

View File

@ -15,7 +15,7 @@
import chunter, { chunterId } from '@hcengineering/chunter'
import type { Client, Space } from '@hcengineering/core'
import type { IntlString, Resource } from '@hcengineering/platform'
import type { IntlString, Metadata, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import { ViewAction } from '@hcengineering/view'
@ -87,5 +87,8 @@ export default mergeIds(chunterId, chunter, {
Messages: '' as IntlString,
NoResults: '' as IntlString,
CopyLink: '' as IntlString
},
metadata: {
Draft: '' as Metadata<Record<string, any>>
}
})