Attachments in comments (#1077)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-03-01 20:14:51 +06:00 committed by GitHub
parent 0d7857fe99
commit 2a061d58ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 305 additions and 18 deletions

View File

@ -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",

View File

@ -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)

View File

@ -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;

View File

@ -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"
}

View File

@ -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} />

View File

@ -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>

View File

@ -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>

View File

@ -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: {

View File

@ -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"

View File

@ -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">

View File

@ -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">

View File

@ -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} />
<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage}/>

View File

@ -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;

View File

@ -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>

View File

@ -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
}
/**