From 2a061d58ec7998c02e1cc1c32a83e6405a866a00 Mon Sep 17 00:00:00 2001 From: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com> Date: Tue, 1 Mar 2022 20:14:51 +0600 Subject: [PATCH] Attachments in comments (#1077) Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com> --- models/chunter/package.json | 1 + models/chunter/src/index.ts | 9 +- .../src/components/ReferenceInput.svelte | 10 +- plugins/attachment-resources/package.json | 1 + .../src/components/AttachmentDocList.svelte | 41 +++++ .../src/components/AttachmentList.svelte | 47 ++++++ .../src/components/AttachmentRefInput.svelte | 156 ++++++++++++++++++ plugins/attachment-resources/src/index.ts | 5 +- plugins/chunter-resources/package.json | 2 + .../src/components/Channel.svelte | 3 +- .../src/components/ChannelView.svelte | 19 ++- .../src/components/CommentInput.svelte | 14 +- .../src/components/Message.svelte | 11 +- .../activity/TxCommentCreate.svelte | 2 + plugins/chunter/src/index.ts | 2 + 15 files changed, 305 insertions(+), 18 deletions(-) create mode 100644 plugins/attachment-resources/src/components/AttachmentDocList.svelte create mode 100644 plugins/attachment-resources/src/components/AttachmentList.svelte create mode 100644 plugins/attachment-resources/src/components/AttachmentRefInput.svelte diff --git a/models/chunter/package.json b/models/chunter/package.json index 95350ee8e1..0966f060ce 100644 --- a/models/chunter/package.json +++ b/models/chunter/package.json @@ -29,6 +29,7 @@ "@anticrm/model": "~0.6.0", "@anticrm/ui": "~0.6.0", "@anticrm/view": "~0.6.0", + "@anticrm/model-attachment": "~0.6.0", "@anticrm/chunter": "~0.6.0", "@anticrm/chunter-resources": "~0.6.0", "@anticrm/platform": "~0.6.5", diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index 88db467d51..f75c815df5 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -17,12 +17,13 @@ import activity from '@anticrm/activity' import type { Backlink, Channel, Comment, Message } from '@anticrm/chunter' import type { Class, Doc, Domain, Ref } from '@anticrm/core' import { IndexKind } from '@anticrm/core' -import { Builder, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model' +import { Builder, Collection, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model' import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core' import view from '@anticrm/model-view' import workbench from '@anticrm/model-workbench' import { ObjectDDParticipant } from '@anticrm/view' import chunter from './plugin' +import attachment from '@anticrm/model-attachment' export const DOMAIN_CHUNTER = 'chunter' as Domain export const DOMAIN_COMMENT = 'comment' as Domain @@ -36,6 +37,9 @@ export class TMessage extends TDoc implements Message { @Prop(TypeMarkup(), chunter.string.Content) @Index(IndexKind.FullText) content!: string + + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments) + attachments?: number } @Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT) @@ -44,6 +48,9 @@ export class TComment extends TAttachedDoc implements Comment { @Prop(TypeMarkup(), chunter.string.Message) @Index(IndexKind.FullText) message!: string + + @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments) + attachments?: number } @Model(chunter.class.Backlink, chunter.class.Comment) diff --git a/packages/text-editor/src/components/ReferenceInput.svelte b/packages/text-editor/src/components/ReferenceInput.svelte index 8faadaf014..edf13f643e 100644 --- a/packages/text-editor/src/components/ReferenceInput.svelte +++ b/packages/text-editor/src/components/ReferenceInput.svelte @@ -34,6 +34,7 @@ const dispatch = createEventDispatcher() export let content: string = '' export let showSend = true + export let withoutTopBorder = false const client = getClient() let textEditor: TextEditor @@ -53,7 +54,7 @@ { label: textEditorPlugin.string.Attach, icon: Attach, - action: () => {}, + action: () => { dispatch('attach') }, order: 1000 }, { @@ -156,7 +157,7 @@ </script> <div class="ref-container"> - <div class="textInput"> + <div class="textInput" class:withoutTopBorder> <div class="inputMsg"> <TextEditor bind:content={content} bind:this={textEditor} on:content={ ev => { @@ -195,6 +196,11 @@ border: 1px solid var(--theme-bg-accent-color); border-radius: .75rem; + &.withoutTopBorder { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + .inputMsg { display: flex; align-self: center; diff --git a/plugins/attachment-resources/package.json b/plugins/attachment-resources/package.json index 3e19ea39d3..8c935e23d1 100644 --- a/plugins/attachment-resources/package.json +++ b/plugins/attachment-resources/package.json @@ -40,6 +40,7 @@ "@anticrm/view": "~0.6.0", "@anticrm/view-resources": "~0.6.0", "@anticrm/panel": "~0.6.0", + "@anticrm/text-editor": "~0.6.0", "@anticrm/login": "~0.6.1", "filesize": "^8.0.3" } diff --git a/plugins/attachment-resources/src/components/AttachmentDocList.svelte b/plugins/attachment-resources/src/components/AttachmentDocList.svelte new file mode 100644 index 0000000000..9ba1dcb138 --- /dev/null +++ b/plugins/attachment-resources/src/components/AttachmentDocList.svelte @@ -0,0 +1,41 @@ +<!-- +// 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 { Attachment } from '@anticrm/attachment' + import type { Doc } from '@anticrm/core' + import { createQuery } from '@anticrm/presentation' + import attachment from '../plugin' + import AttachmentList from './AttachmentList.svelte' + + export let value: Doc & { attachments?: number } + + const query = createQuery() + let attachments: Attachment[] = [] + + $: updateQuery(value) + + function updateQuery (value: Doc & { attachments?: number }): void { + if (value && value.attachments && value.attachments > 0) { + query.query(attachment.class.Attachment, { + attachedTo: value._id + }, (res) => attachments = res) + } else { + attachments = [] + } + } +</script> + +<AttachmentList {attachments} /> diff --git a/plugins/attachment-resources/src/components/AttachmentList.svelte b/plugins/attachment-resources/src/components/AttachmentList.svelte new file mode 100644 index 0000000000..830b710790 --- /dev/null +++ b/plugins/attachment-resources/src/components/AttachmentList.svelte @@ -0,0 +1,47 @@ +<!-- +// 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 { Attachment } from '@anticrm/attachment' + import AttachmentPresenter from './AttachmentPresenter.svelte' + + export let attachments: Attachment[] = [] +</script> + +{#if attachments.length} + <div class='container'> + {#each attachments as attachment} + <div class='item'> + <AttachmentPresenter value={attachment} /> + </div> + {/each} + </div> +{/if} + +<style lang="scss"> + .container { + background-color: var(--theme-bg-accent-color); + border: 1px solid var(--theme-bg-accent-color); + border-radius: 0.75rem; + + .item { + padding: 1rem; + } + + .item + .item { + border-top: 1px solid var(--theme-bg-accent-color); + } + } +</style> diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte new file mode 100644 index 0000000000..d1397507b1 --- /dev/null +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -0,0 +1,156 @@ + +<!-- +// 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 { createQuery, getClient } from '@anticrm/presentation' + import { ReferenceInput } from '@anticrm/text-editor' + import { deleteFile, uploadFile } from '../utils' + import attachment from '../plugin' + import { setPlatformStatus, unknownError } from '@anticrm/platform' + import { createEventDispatcher, onDestroy } from 'svelte' + import { Class, Doc, Ref, Space } from '@anticrm/core' + import { Attachment } from '@anticrm/attachment' + import AttachmentPresenter from './AttachmentPresenter.svelte' + import { IconClose } from '@anticrm/ui' +import ActionIcon from '@anticrm/ui/src/components/ActionIcon.svelte'; + + export let objectId: Ref<Doc> + export let space: Ref<Space> + export let _class: Ref<Class<Doc>> + + let inputFile: HTMLInputElement + let saved = false + const dispatch = createEventDispatcher() + + const client = getClient() + const query = createQuery() + let attachments: Attachment[] = [] + + $: objectId && query.query(attachment.class.Attachment, { + attachedTo: objectId + }, (res) => attachments = res) + + async function createAttachment (file: File) { + try { + const uuid = await uploadFile(file, { space, attachedTo: objectId }) + console.log('uploaded file uuid', uuid) + await client.addCollection(attachment.class.Attachment, space, objectId, _class, 'attachments', { + name: file.name, + file: uuid, + type: file.type, + size: file.size, + lastModified: file.lastModified + }) + } catch (err: any) { + setPlatformStatus(unknownError(err)) + } + } + + function fileSelected () { + const list = inputFile.files + if (list === null || list.length === 0) return + for (let index = 0; index < list.length; index++) { + const file = list.item(index) + if (file !== null) createAttachment(file) + } + } + + function fileDrop (e: DragEvent) { + const list = e.dataTransfer?.files + if (list === undefined || list.length === 0) return + for (let index = 0; index < list.length; index++) { + const file = list.item(index) + if (file !== null) createAttachment(file) + } + } + + async function removeAttachment (attachment: Attachment): Promise<void> { + await client.removeCollection(attachment._class, attachment.space, attachment._id, attachment.attachedTo, attachment.attachedToClass, 'attachments') + await deleteFile(attachment.file) + } + + onDestroy(() => { + if (!saved) { + attachments.map((attachment) => { + removeAttachment(attachment) + }) + } + }) + + async function onMessage (event: CustomEvent) { + saved = true + dispatch('message', { message: event.detail, attachments: attachments.length }) + } + +</script> + +<input + bind:this={inputFile} + multiple + type="file" + name="file" + id="file" + style="display: none" + on:change={fileSelected} + /> +<div class="container" + on:dragover|preventDefault={() => {}} + on:dragleave={() => {}} + on:drop|preventDefault|stopPropagation={fileDrop} + > + {#if attachments.length} + <div class='flex-row-center list'> + {#each attachments as attachment} + <div class='item flex'> + <AttachmentPresenter value={attachment} /> + <div class='remove'> + <ActionIcon icon={IconClose} action={() => { removeAttachment(attachment) }} size='small' /> + </div> + </div> + {/each} + </div> + {/if} + <ReferenceInput on:message={onMessage} withoutTopBorder={attachments.length > 0} on:attach={() => { inputFile.click() }} /> +</div> + +<style lang="scss"> + .list { + padding: 1rem; + color: var(--theme-caption-color); + overflow-x: auto; + background-color: var(--theme-bg-accent-color); + border: 1px solid var(--theme-bg-accent-color); + border-radius: .75rem; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + .item + .item { + padding-left: 1rem; + border-left: 1px solid var(--theme-bg-accent-color); + } + + .item { + .remove { + visibility: hidden; + } + } + + .item:hover { + .remove { + visibility: visible; + } + } + } +</style> diff --git a/plugins/attachment-resources/src/index.ts b/plugins/attachment-resources/src/index.ts index 588b925e84..eee6da65e9 100644 --- a/plugins/attachment-resources/src/index.ts +++ b/plugins/attachment-resources/src/index.ts @@ -15,13 +15,16 @@ import AttachmentsPresenter from './components/AttachmentsPresenter.svelte' import AttachmentPresenter from './components/AttachmentPresenter.svelte' +import AttachmentDocList from './components/AttachmentDocList.svelte' +import AttachmentList from './components/AttachmentList.svelte' +import AttachmentRefInput from './components/AttachmentRefInput.svelte' import TxAttachmentCreate from './components/activity/TxAttachmentCreate.svelte' import Attachments from './components/Attachments.svelte' import Photos from './components/Photos.svelte' import { Resources } from '@anticrm/platform' import { uploadFile, deleteFile } from './utils' -export { Attachments, AttachmentsPresenter } +export { Attachments, AttachmentsPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList } export default async (): Promise<Resources> => ({ component: { diff --git a/plugins/chunter-resources/package.json b/plugins/chunter-resources/package.json index 292816ff4d..b6b741b3ce 100644 --- a/plugins/chunter-resources/package.json +++ b/plugins/chunter-resources/package.json @@ -41,6 +41,8 @@ "@anticrm/text-editor": "~0.6.0", "@anticrm/contact": "~0.6.2", "@anticrm/contact-resources": "~0.6.0", + "@anticrm/attachment": "~0.6.0", + "@anticrm/attachment-resources": "~0.6.0", "@anticrm/view-resources": "~0.6.0", "@anticrm/view": "~0.6.0", "@anticrm/workbench": "~0.6.1" diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte index 840b375e66..3e028c5cb7 100644 --- a/plugins/chunter-resources/src/components/Channel.svelte +++ b/plugins/chunter-resources/src/components/Channel.svelte @@ -17,6 +17,7 @@ import type { Ref, Space } from '@anticrm/core' import { createQuery } from '@anticrm/presentation' import type { Message } from '@anticrm/chunter' + import attachment from '@anticrm/attachment' import chunter from '../plugin' import { default as MessageComponent } from './Message.svelte' @@ -26,7 +27,7 @@ let messages: Message[] | undefined const query = createQuery() - $: query.query(chunter.class.Message, { space }, result => { messages = result }) + $: query.query(chunter.class.Message, { space }, result => { messages = result }, { lookup: { _id: { attachments: attachment.class.Attachment } }}) </script> <div class="flex-col container"> diff --git a/plugins/chunter-resources/src/components/ChannelView.svelte b/plugins/chunter-resources/src/components/ChannelView.svelte index 7daf02d575..9b62250b6c 100644 --- a/plugins/chunter-resources/src/components/ChannelView.svelte +++ b/plugins/chunter-resources/src/components/ChannelView.svelte @@ -14,25 +14,30 @@ --> <script lang="ts"> - import type { Ref, Space } from '@anticrm/core' + import { generateId, Ref, Space } from '@anticrm/core' import chunter from '../plugin' import { getClient } from '@anticrm/presentation' import Channel from './Channel.svelte' - import { ReferenceInput } from '@anticrm/text-editor' + import { AttachmentRefInput } from '@anticrm/attachment-resources' import { createBacklinks } from '../backlinks' export let space: Ref<Space> const client = getClient() + const _class = chunter.class.Message + let _id = generateId() async function onMessage (event: CustomEvent) { - const msgRef = await client.createDoc(chunter.class.Message, space, { - content: event.detail - }) + const { message, attachments } = event.detail + await client.createDoc(_class, space, { + content: message, + attachments + }, _id) // Create an backlink to document - await createBacklinks(client, space, chunter.class.Channel, msgRef, event.detail) + await createBacklinks(client, space, chunter.class.Channel, _id, message) + _id = generateId() } </script> @@ -40,7 +45,7 @@ <Channel {space} /> </div> <div class="reference"> - <ReferenceInput on:message={onMessage}/> + <AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/> </div> <style lang="scss"> diff --git a/plugins/chunter-resources/src/components/CommentInput.svelte b/plugins/chunter-resources/src/components/CommentInput.svelte index 7c784bf264..1dc479ae24 100644 --- a/plugins/chunter-resources/src/components/CommentInput.svelte +++ b/plugins/chunter-resources/src/components/CommentInput.svelte @@ -16,21 +16,25 @@ --> <script lang="ts"> import { Comment } from '@anticrm/chunter' - import { Doc } from '@anticrm/core' + import { Doc, generateId, Ref } from '@anticrm/core' import { getClient } from '@anticrm/presentation' - import { ReferenceInput } from '@anticrm/text-editor' + import { AttachmentRefInput } from '@anticrm/attachment-resources' import { createBacklinks } from '../backlinks' import chunter from '../plugin' const client = getClient() export let object: Doc + const _class = chunter.class.Comment + let _id: Ref<Comment> = generateId() async function onMessage (event: CustomEvent) { - const commentId = await client.addCollection<Doc, Comment>(chunter.class.Comment, object.space, object._id, object._class, 'comments', { message: event.detail }) + const { message, attachments } = event.detail + await client.addCollection<Doc, Comment>(_class, object.space, object._id, object._class, 'comments', { message, attachments }, _id) // Create an backlink to document - await createBacklinks(client, object._id, object._class, commentId, event.detail) + await createBacklinks(client, object._id, object._class, _id, message) + _id = generateId() } </script> -<ReferenceInput on:message={onMessage} /> \ No newline at end of file +<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage}/> \ No newline at end of file diff --git a/plugins/chunter-resources/src/components/Message.svelte b/plugins/chunter-resources/src/components/Message.svelte index 7454a9083d..d68e0f0bf1 100644 --- a/plugins/chunter-resources/src/components/Message.svelte +++ b/plugins/chunter-resources/src/components/Message.svelte @@ -26,14 +26,19 @@ import { MessageViewer } from '@anticrm/presentation' import { getTime, getUser } from '../utils' import { formatName } from '@anticrm/contact' + import { AttachmentList } from '@anticrm/attachment-resources' + import { WithLookup } from '@anticrm/core' + import { Attachment } from '@anticrm/attachment' - export let message: Message + export let message: WithLookup<Message> let reactions: boolean = false let replies: boolean = false let thread: boolean = false const client = getClient() + + $: attachments = (message.$lookup?.attachments ?? []) as Attachment[] </script> <div class="container"> @@ -46,6 +51,7 @@ <span>{getTime(message.modifiedOn)}</span> </div> <div class="text"><MessageViewer message={message.content}/></div> + <div class="attachments"><AttachmentList {attachments} /></div> {#if (reactions || replies) && !thread} <div class="footer"> <div>{#if reactions}<Reactions/>{/if}</div> @@ -96,6 +102,9 @@ .text { line-height: 150%; } + .attachments { + margin-top: 1rem; + } .footer { display: flex; justify-content: space-between; diff --git a/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte b/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte index 31f3479537..d8f54b3225 100644 --- a/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte +++ b/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte @@ -17,6 +17,7 @@ import type { Comment } from '@anticrm/chunter' import type { TxCreateDoc } from '@anticrm/core' import { getClient, MessageViewer } from '@anticrm/presentation' + import { AttachmentDocList } from '@anticrm/attachment-resources' import { ReferenceInput } from '@anticrm/text-editor' import { Button } from '@anticrm/ui' import { createEventDispatcher } from 'svelte' @@ -55,6 +56,7 @@ </div> {:else} <MessageViewer message={value.message}/> + <AttachmentDocList {value} /> {/if} </div> diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 685f529ec5..7e53545452 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -28,6 +28,7 @@ export interface Channel extends Space {} */ export interface Message extends Doc { content: string + attachments?: number } /** @@ -35,6 +36,7 @@ export interface Message extends Doc { */ export interface Comment extends AttachedDoc { message: string + attachments?: number } /**