Gmail attachments (#1095)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-03-04 15:09:01 +06:00 committed by GitHub
parent 35b8fbe9c5
commit 906f741d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 232 additions and 47 deletions

View File

@ -33,6 +33,7 @@
"@anticrm/model-contact": "~0.6.1",
"@anticrm/gmail": "~0.6.0",
"@anticrm/gmail-resources": "~0.6.0",
"@anticrm/model-attachment": "~0.6.0",
"@anticrm/setting": "~0.6.0",
"@anticrm/ui": "~0.6.0"
}

View File

@ -16,10 +16,11 @@
import activity from '@anticrm/activity'
import { Domain, IndexKind, Type } from '@anticrm/core'
import type { Message, SharedMessage, SharedMessages } from '@anticrm/gmail'
import { ArrOf, Builder, Index, Model, Prop, TypeBoolean, TypeString } from '@anticrm/model'
import type { Message, NewMessage, SharedMessage, SharedMessages } from '@anticrm/gmail'
import { ArrOf, Builder, Collection, Index, Model, Prop, TypeBoolean, TypeString } from '@anticrm/model'
import contact from '@anticrm/model-contact'
import core, { TAttachedDoc } from '@anticrm/model-core'
import core, { TDoc, TAttachedDoc } from '@anticrm/model-core'
import attachment from '@anticrm/model-attachment'
import setting from '@anticrm/setting'
import gmail from './plugin'
@ -46,10 +47,6 @@ export class TMessage extends TAttachedDoc implements Message {
@Index(IndexKind.FullText)
to!: string
@Prop(TypeString(), contact.string.Contact)
@Index(IndexKind.FullText)
contact!: string
@Prop(TypeString(), gmail.string.Subject)
@Index(IndexKind.FullText)
subject!: string
@ -67,6 +64,37 @@ export class TMessage extends TAttachedDoc implements Message {
@Prop(TypeBoolean(), gmail.string.Incoming)
incoming!: boolean
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
}
@Model(gmail.class.NewMessage, core.class.Doc, DOMAIN_GMAIL)
export class TNewMessage extends TDoc implements NewMessage {
@Prop(TypeString(), gmail.string.ReplyTo)
@Index(IndexKind.FullText)
replyTo?: string
@Prop(TypeString(), gmail.string.To)
@Index(IndexKind.FullText)
to!: string
@Prop(TypeString(), gmail.string.Subject)
@Index(IndexKind.FullText)
subject!: string
@Prop(TypeString(), gmail.string.Message)
@Index(IndexKind.FullText)
content!: string
@Prop(TypeString(), gmail.string.Status)
status!: 'new' | 'sent'
@Prop(ArrOf(TypeString()), gmail.string.Copy)
copy?: string[]
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
attachments?: number
}
@Model(gmail.class.SharedMessages, core.class.AttachedDoc, DOMAIN_GMAIL)
@ -76,7 +104,7 @@ export class TSharedMessages extends TAttachedDoc implements SharedMessages {
}
export function createModel (builder: Builder): void {
builder.createModel(TMessage, TSharedMessages)
builder.createModel(TMessage, TSharedMessages, TNewMessage)
builder.createDoc(
contact.class.ChannelProvider,

View File

@ -33,6 +33,7 @@ export default mergeIds(gmailId, gmail, {
Messages: '' as IntlString,
Incoming: '' as IntlString,
Email: '' as IntlString,
Status: '' as IntlString,
EmailPlaceholder: '' as IntlString
},
ids: {

View File

@ -24,7 +24,7 @@
import { Attachment } from '@anticrm/attachment'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import { IconClose } from '@anticrm/ui'
import ActionIcon from '@anticrm/ui/src/components/ActionIcon.svelte';
import { ActionIcon } from '@anticrm/ui'
export let objectId: Ref<Doc>
export let space: Ref<Space>

View File

@ -24,7 +24,7 @@ import Photos from './components/Photos.svelte'
import { Resources } from '@anticrm/platform'
import { uploadFile, deleteFile } from './utils'
export { Attachments, AttachmentsPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
export { Attachments, AttachmentsPresenter, AttachmentPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
export default async (): Promise<Resources> => ({
component: {

View File

@ -27,6 +27,7 @@
"Messages": "Messages",
"Incoming": "Incoming",
"Email": "Email",
"Status": "Status",
"EmailPlaceholder": "john.appleseed@apple.com"
}
}

View File

@ -27,6 +27,7 @@
"Messages": "Сообщения",
"Incoming": "Входящие",
"Email": "Email",
"Status": "Статус",
"EmailPlaceholder": "john.appleseed@apple.com"
}
}

View File

@ -42,6 +42,8 @@
"@anticrm/chunter": "~0.6.0",
"@anticrm/chunter-resources": "~0.6.0",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/attachment": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/login": "~0.6.1",
"@anticrm/core": "~0.6.11"
}

View File

@ -20,6 +20,9 @@
import { IconArrowLeft, Label } from '@anticrm/ui'
import gmail from '../plugin'
import FullMessageContent from './FullMessageContent.svelte'
import { createQuery } from '@anticrm/presentation'
import attachment, { Attachment } from '@anticrm/attachment'
import { AttachmentPresenter } from '@anticrm/attachment-resources'
export let currentMessage: SharedMessage
export let newMessage: boolean
@ -29,6 +32,13 @@
const dispatch = createEventDispatcher()
const query = createQuery()
let attachments: Attachment[] = []
$: currentMessage._id && query.query(attachment.class.Attachment, {
attachedTo: currentMessage._id
}, (res) => attachments = res)
$: title = currentMessage.incoming ? currentMessage.sender : currentMessage.receiver
$: user = currentMessage.incoming ? currentMessage.receiver : currentMessage.sender
</script>
@ -60,13 +70,22 @@
/>
</div>
</div>
<div class="flex-col clear-mins right-content">
<div class="flex-col clear-mins content">
<Label label={currentMessage.incoming ? gmail.string.To : gmail.string.From} />
{user}
{#if currentMessage.copy?.length}
<Label label={gmail.string.Copy} />: {currentMessage.copy.join(', ')}
{/if}
<div class="flex-col h-full clear-mins mt-9">
{#if attachments.length}
<div class='flex-row-center list mt-2'>
{#each attachments as attachment}
<div class='item flex'>
<AttachmentPresenter value={attachment} />
</div>
{/each}
</div>
{/if}
<div class="flex-col h-full clear-mins mt-4">
<FullMessageContent content={currentMessage.content} />
</div>
</div>
@ -90,8 +109,23 @@
}
}
.right-content {
.content {
flex-grow: 1;
padding: 1.5rem 2.5rem;
.list {
padding: 1rem;
color: var(--theme-caption-color);
overflow-x: auto;
overflow-y: hidden;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
.item + .item {
padding-left: 1rem;
border-left: 1px solid var(--theme-bg-accent-color);
}
}
}
</style>

View File

@ -15,6 +15,7 @@
-->
<script lang="ts">
import type { SharedMessage } from '@anticrm/gmail'
import { AttachmentsPresenter } from '@anticrm/attachment-resources'
import { CheckBox, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { getTime } from '../utils'
@ -41,7 +42,10 @@
<div class="content-trans-color overflow-label mr-4">
<Label label={gmail.string.From} /><span class="content-accent-color">{message.sender}</span>
</div>
<div class="content-trans-color">{getTime(message.modifiedOn)}</div>
<div class="content-trans-color flex">
<AttachmentsPresenter value={message} />
{getTime(message.modifiedOn)}
</div>
</div>
<div class="content-trans-color text-sm overflow-label mr-4 mb-4">
<Label label={gmail.string.To} /><span class="content-accent-color">{message.receiver}</span>

View File

@ -1,6 +1,5 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// 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
@ -14,54 +13,112 @@
// limitations under the License.
-->
<script lang="ts">
import { getMetadata } from '@anticrm/platform'
import login from '@anticrm/login'
import { NewMessage, SharedMessage } from '@anticrm/gmail'
import EditBox from '@anticrm/ui/src/components/EditBox.svelte'
import Button from '@anticrm/ui/src/components/Button.svelte'
import { createEventDispatcher } from 'svelte'
import { IconArrowLeft, Label } from '@anticrm/ui'
import { Channel, Contact, formatName } from '@anticrm/contact'
import { TextEditor } from '@anticrm/text-editor'
import plugin from '../plugin'
import attachmentP,{ Attachment } from '@anticrm/attachment'
import { AttachmentPresenter } from '@anticrm/attachment-resources'
import { Channel,Contact,formatName } from '@anticrm/contact'
import { Data,generateId } from '@anticrm/core'
import { NewMessage,SharedMessage } from '@anticrm/gmail'
import { NotificationClientImpl } from '@anticrm/notification-resources'
import { getResource,setPlatformStatus,unknownError } from '@anticrm/platform'
import { createQuery,getClient } from '@anticrm/presentation'
import { TextEditor } from '@anticrm/text-editor'
import { ActionIcon,IconArrowLeft,IconAttachment,IconClose,Label } from '@anticrm/ui'
import Button from '@anticrm/ui/src/components/Button.svelte'
import EditBox from '@anticrm/ui/src/components/EditBox.svelte'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
export let object: Contact
export let channel: Channel
export let currentMessage: SharedMessage | undefined
const client = getClient()
const notificationClient = NotificationClientImpl.getClient()
let objectId = generateId()
let editor: TextEditor
let copy: string = ''
const obj: NewMessage = {
const obj: Data<NewMessage> = {
subject: currentMessage ? 'RE: ' + currentMessage.subject : '',
content: '',
to: channel.value,
replyTo: currentMessage?.messageId
replyTo: currentMessage?.messageId,
status: 'new'
}
const url = getMetadata(login.metadata.GmailUrl) ?? ''
async function sendMsg () {
await fetch(url + '/send', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(login.metadata.LoginToken),
'Content-Type': 'application/json'
},
body: JSON.stringify({
...obj,
copy: copy.split(',').map((m) => m.trim())
})
})
await client.createDoc(plugin.class.NewMessage, plugin.space.Gmail, {
...obj,
attachments: attachments.length,
copy: copy.split(',').map((m) => m.trim()).filter((m) => m.length)
}, objectId)
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
objectId = generateId()
dispatch('close')
}
const dispatch = createEventDispatcher()
let inputFile: HTMLInputElement
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 createAttachment (file: File) {
try {
const uploadFile = await getResource(attachmentP.helper.UploadFile)
const uuid = await uploadFile(file, { space: plugin.space.Gmail, attachedTo: objectId })
console.log('uploaded file uuid', uuid)
await client.addCollection(attachmentP.class.Attachment, plugin.space.Gmail, objectId, plugin.class.NewMessage, 'attachments', {
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
})
} catch (err: any) {
setPlatformStatus(unknownError(err))
}
}
const query = createQuery()
async function removeAttachment (attachment: Attachment): Promise<void> {
const deleteFile = await getResource(attachmentP.helper.DeleteFile)
await client.removeCollection(attachment._class, attachment.space, attachment._id, attachment.attachedTo, attachment.attachedToClass, 'attachments')
await deleteFile(attachment.file)
}
let attachments: Attachment[] = []
$: objectId && query.query(attachmentP.class.Attachment, {
attachedTo: objectId
}, (res) => attachments = res)
</script>
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
<div class="flex-between clear-mins header">
<div
class="flex-center icon"
@ -78,11 +135,18 @@
<span class="content-accent-color">{formatName(object.name)} ({channel.value})</span>
</div>
</div>
<div class="mr-3">
<div class="mr-3 flex-row-center">
<div class="mr-2">
<ActionIcon icon={IconAttachment} size={'small'} action={() => {inputFile.click()}} />
</div>
<Button label={plugin.string.Send} size={'small'} primary on:click={sendMsg} />
</div>
</div>
<div class="flex-col clear-mins right-content">
<div class="flex-col clear-mins right-content"
on:dragover|preventDefault={() => {}}
on:dragleave={() => {}}
on:drop|preventDefault|stopPropagation={fileDrop}
>
<div class="mb-2">
<EditBox
label={plugin.string.Subject}
@ -91,7 +155,7 @@
maxWidth={'min-content'}
/>
</div>
<div class="mb-4">
<div>
<EditBox
label={plugin.string.Copy}
bind:value={copy}
@ -99,7 +163,19 @@
maxWidth={'min-content'}
/>
</div>
<div class="input clear-mins">
{#if attachments.length}
<div class='flex-row-center list mt-2'>
{#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}
<div class="input mt-4 clear-mins">
<TextEditor bind:this={editor} bind:content={obj.content} on:blur={editor.submit} />
</div>
</div>
@ -127,6 +203,33 @@
flex-grow: 1;
padding: 1.5rem 2.5rem;
.list {
padding: 1rem;
color: var(--theme-caption-color);
overflow-x: auto;
overflow-y: hidden;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
.item + .item {
padding-left: 1rem;
border-left: 1px solid var(--theme-bg-accent-color);
}
.item {
.remove {
visibility: hidden;
}
}
.item:hover {
.remove {
visibility: visible;
}
}
}
.input {
overflow: auto;
padding: 1rem;

View File

@ -22,7 +22,7 @@ import type { IntegrationType, Handler } from '@anticrm/setting'
/**
* @public
*/
export interface Message extends NewMessage, AttachedDoc {
export interface Message extends BaseMessage, AttachedDoc {
messageId: string
from: string
textContent: string
@ -32,12 +32,20 @@ export interface Message extends NewMessage, AttachedDoc {
/**
* @public
*/
export interface NewMessage {
export interface BaseMessage extends Doc {
replyTo?: string
to: string
subject: string
content: string
copy?: string[]
attachments?: number
}
/**
* @public
*/
export interface NewMessage extends BaseMessage {
status: 'new' | 'sent'
}
/**
@ -51,6 +59,7 @@ export interface SharedMessage extends Doc {
receiver: string
incoming: boolean
textContent: string
attachments?: number
copy?: string[]
}
@ -80,6 +89,7 @@ export default plugin(gmailId, {
},
class: {
Message: '' as Ref<Class<Message>>,
NewMessage: '' as Ref<Class<NewMessage>>,
SharedMessages: '' as Ref<Class<SharedMessages>>,
SharedMessage: '' as Ref<Class<SharedMessage>>
},