Board: add attachments support

Signed-off-by: Anna No <anna.no@xored.com>
This commit is contained in:
Anna No 2022-04-13 00:28:46 +07:00
parent 13750963c8
commit b09466453d
No known key found for this signature in database
GPG Key ID: 08C11FFC23177C87
15 changed files with 289 additions and 126 deletions

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { Class, Doc, Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { CircleButton, IconAdd } from '@anticrm/ui'
import { createAttachment } from '../utils'
export let loading: number = 0
export let inputFile: HTMLInputElement
export let objectClass: Ref<Class<Doc>>
export let objectId: Ref<Doc>
export let space: Ref<Space>
const client = getClient()
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(client, file, loading, { objectClass, objectId, space })
}
}
function openFile() {
inputFile.click()
}
</script>
<div>
{#if $$slots.control}
<div on:click={openFile}>
<slot name="control"/>
</div>
{:else}
<CircleButton
icon={IconAdd}
size="small"
selected
on:click={openFile} />
{/if}
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected} />
</div>

View File

@ -0,0 +1,51 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { Class, Doc, Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { createAttachment } from '../utils'
export let loading: number = 0
export let objectClass: Ref<Class<Doc>>
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let dragover = false
const client = getClient()
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(client, file, loading, { objectClass, objectId, space })
}
}
</script>
<div
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={fileDrop}>
<slot />
</div>

View File

@ -15,119 +15,55 @@
-->
<script lang="ts">
import { Class, Doc, Ref, Space } from '@anticrm/core'
import { setPlatformStatus, unknownError } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import { CircleButton, IconAdd, Label, Spinner } from '@anticrm/ui'
import { Label, Spinner } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import attachment from '../plugin'
import { uploadFile } from '../utils'
import AddAttachment from './AddAttachment.svelte'
import AttachmentDroppable from './AttachmentDroppable.svelte'
import UploadDuo from './icons/UploadDuo.svelte'
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export let attachments: number | undefined = undefined
let inputFile: HTMLInputElement
let loading = 0
const client = getClient()
async function createAttachment (file: File) {
loading++
try {
const uuid = await uploadFile(file, { space, attachedTo: objectId })
console.log('uploaded file uuid', uuid)
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))
} finally {
loading--
}
}
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)
}
}
let dragover = false
</script>
<div class="attachments-container">
<div class="flex-row-center">
<span class="title"><Label label={attachment.string.Attachments} /></span>
{#if loading}
<Spinner />
{:else}
<CircleButton
icon={IconAdd}
size={'small'}
selected
on:click={() => {
inputFile.click()
}}
/>
{/if}
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
<AddAttachment bind:loading bind:inputFile objectClass={_class} {objectId} {space} />
</div>
{#if (attachments === 0) && !loading}
<div
class="flex-col-center mt-5 zone-container"
class:solid={dragover}
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={fileDrop}
>
<UploadDuo size={'large'} />
<div class="text-sm content-dark-color mt-2">
<Label label={attachment.string.NoAttachments} />
{#if loading}
<Spinner />
{:else if attachments === 0}
<AttachmentDroppable bind:loading bind:dragover objectClass={_class} {objectId} {space}>
<div class="flex-col-center mt-5 zone-container" class:solid={dragover}>
<UploadDuo size={'large'} />
<div class="text-sm content-dark-color mt-2">
<Label label={attachment.string.NoAttachments} />
</div>
<div class="text-sm">
<div class="over-underline" on:click={() => inputFile.click()}>
<Label label={attachment.string.UploadDropFilesHere} />
</div>
</div>
</div>
<div class="text-sm">
<div class='over-underline' on:click={() => inputFile.click()}><Label label={attachment.string.UploadDropFilesHere} /></div>
</div>
</div>
</AttachmentDroppable>
{:else}
<Table
_class={attachment.class.Attachment}
config={['', 'lastModified']}
options={{}}
query={{ attachedTo: objectId }}
loadingProps={ { length: attachments ?? 0 } }
/>
loadingProps={{ length: attachments ?? 0 }} />
{/if}
</div>
@ -150,6 +86,9 @@
background: var(--theme-bg-accent-color);
border: 1px dashed var(--theme-zone-border-lite);
border-radius: 0.75rem;
&.solid { border-style: solid; }
&.solid {
border-style: solid;
}
}
</style>

View File

@ -13,6 +13,8 @@
// limitations under the License.
//
import AddAttachment from './components/AddAttachment.svelte'
import AttachmentDroppable from './components/AttachmentDroppable.svelte'
import AttachmentsPresenter from './components/AttachmentsPresenter.svelte'
import AttachmentPresenter from './components/AttachmentPresenter.svelte'
import AttachmentDocList from './components/AttachmentDocList.svelte'
@ -24,7 +26,7 @@ import Photos from './components/Photos.svelte'
import { Resources } from '@anticrm/platform'
import { uploadFile, deleteFile } from './utils'
export { Attachments, AttachmentsPresenter, AttachmentPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
export { AddAttachment, AttachmentDroppable, Attachments, AttachmentsPresenter, AttachmentPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
export default async (): Promise<Resources> => ({
component: {

View File

@ -14,9 +14,11 @@
// limitations under the License.
//
import type { Doc, Ref, Space } from '@anticrm/core'
import type { Class, Doc, Ref, Space, TxOperations as Client } from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
import { getMetadata, setPlatformStatus, unknownError } from '@anticrm/platform'
import attachment from './plugin'
export async function uploadFile (file: File, opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }): Promise<string> {
const uploadUrl = getMetadata(login.metadata.UploadUrl)
@ -68,6 +70,26 @@ export async function deleteFile (id: string): Promise<void> {
}
}
export async function createAttachment (client: Client, file: File, loading: number, attachTo: {objectClass: Ref<Class<Doc>>, space: Ref<Space>, objectId: Ref<Doc>}) {
loading++
const {objectClass, objectId, space} = attachTo
try {
const uuid = await uploadFile(file, { space, attachedTo: objectId })
console.log('uploaded file uuid', uuid)
client.addCollection(attachment.class.Attachment, space, objectId, objectClass, 'attachments', {
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
})
} catch (err: any) {
setPlatformStatus(unknownError(err))
} finally {
loading--
}
}
export function getType (type: string): 'image' | 'video' | 'audio' | 'pdf' | 'other' {
if (type.startsWith('image/')) {
return 'image'

View File

@ -33,6 +33,7 @@
"Checklist": "Checklist",
"Dates": "Dates",
"Attachments": "Attachments",
"AddAttachment": "Add an attachment",
"CustomFields": "Custom Fields",
"Automation": "Automation",
"AddButton": "Add Button",

View File

@ -33,6 +33,7 @@
"Checklist": "Списки",
"Dates": "Дата",
"Attachments": "Прикрепленное",
"AddAttachment": "Прикрепить",
"CustomFields": "Дополнительно",
"Automation": "Автоматизация",
"AddButton": "Добавить",

View File

@ -31,6 +31,7 @@
},
"dependencies": {
"@anticrm/activity": "~0.6.0",
"@anticrm/attachment": "~0.6.1",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/board": "~0.6.0",
"@anticrm/chunter": "~0.6.1",

View File

@ -29,6 +29,7 @@
import { updateCard } from '../utils/CardUtils'
import CardActions from './editor/CardActions.svelte'
import CardActivity from './editor/CardActivity.svelte'
import CardAttachments from './editor/CardAttachments.svelte'
import CardDetails from './editor/CardDetails.svelte'
export let _id: Ref<Card>
@ -125,7 +126,7 @@
/>
</div>
</div>
<!-- TODO attachments-->
<CardAttachments value={object} />
<!-- TODO checklists -->
<CardActivity value={object} />
</div>

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentsPresenter } from '@anticrm/attachment-resources'
import { AttachmentDroppable, AttachmentsPresenter } from '@anticrm/attachment-resources'
import type { Card } from '@anticrm/board'
import { CommentsPresenter } from '@anticrm/chunter-resources'
import type { WithLookup } from '@anticrm/core'
@ -26,40 +26,57 @@
export let object: WithLookup<Card>
export let dragged: boolean
let loadingAttachment = 0
let dragoverAttachment = false
function showMenu(ev?: Event): void {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
function showLead() {
function showCard() {
showPanel(board.component.EditCard, object._id, object._class, 'middle')
}
</script>
<div class="flex-between mb-4">
<div class="flex-col">
<div class="fs-title cursor-pointer" on:click={showLead}>{object.title}</div>
</div>
<div class="flex-row-center">
<div class="mr-2">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
<AttachmentDroppable
bind:loading={loadingAttachment}
bind:dragover={dragoverAttachment}
objectClass={object._class}
objectId={object._id}
space={object.space}>
<div class:opacity-overlay={dragoverAttachment}>
<div class="flex-between mb-4">
<div class="flex-col">
<div class="fs-title cursor-pointer" on:click={showCard}>{object.title}</div>
</div>
<div class="flex-row-center">
<div class="mr-2">
<Component is={notification.component.NotificationPresenter} props={{ value: object }} />
</div>
<ActionIcon
label={board.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size="small" />
</div>
</div>
<div class="flex-between">
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75">
<AttachmentsPresenter value={object} />
</div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75">
<CommentsPresenter value={object} />
</div>
{/if}
</div>
<ActionIcon
label={board.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size={'small'}
/>
</div>
</div>
<div class="flex-between">
<div class="flex-row-center">
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
{/if}
{#if (object.comments ?? 0) > 0}
<div class="step-lr75"><CommentsPresenter value={object} /></div>
{/if}
</div>
</div>
</AttachmentDroppable>

View File

@ -34,7 +34,8 @@
let actionGroups: { label: IntlString; actions: CardAction[] }[] = []
getCardActions(client).then(async (result) => {
async function fetch() {
const result = await getCardActions(client)
for (const action of result) {
let supported = true
if (action.supported) {
@ -74,7 +75,9 @@
actions: actions.sort(cardActionSorter)
}
]
})
}
$: fetch()
</script>
{#if value}
@ -99,8 +102,7 @@
const handler = await getResource(action.handler)
handler(value, client)
}
}}
/>
}} />
{/if}
{/each}
</div>

View File

@ -26,8 +26,6 @@
{#if value !== undefined}
<div class="flex-col-stretch h-full w-full">
<!-- TODO attachments-->
<!-- TODO checklists -->
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={IconActivity} size="large" />

View File

@ -0,0 +1,63 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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, { Attachment } from '@anticrm/attachment'
import { AddAttachment } from '@anticrm/attachment-resources'
import type { Card } from '@anticrm/board'
import { getClient } from '@anticrm/presentation'
import { Button, Icon, IconAttachment, Label } from '@anticrm/ui'
import AttachmentPresenter from '../presenters/AttachmentPresenter.svelte'
import board from '../../plugin'
export let value: Card
const client = getClient()
let attachments: Attachment[] = []
let inputFile: HTMLInputElement
async function fetch() {
attachments = await client.findAll(attachment.class.Attachment, { space: value.space, attachedTo: value._id })
}
$: fetch()
</script>
{#if value !== undefined && value.attachments !== undefined && value.attachments > 0}
<div class="flex-col w-full">
<div class="flex-row-streach mt-4 mb-2">
<div class="w-9">
<Icon icon={IconAttachment} size="large" />
</div>
<div class="flex-grow fs-title">
<Label label={board.string.Attachments} />
</div>
</div>
<div class="flex-row-streach">
<div class="w-9" />
<div class="flex-col flex-gap-1 w-full">
{#each attachments as attach}
<AttachmentPresenter value={attach} />
{/each}
<div class="mt-2">
<AddAttachment bind:inputFile objectClass={value._class} objectId={value._id} space={value.space}>
<Button label={board.string.AddAttachment} kind="no-border" slot="control" />
</AddAttachment>
</div>
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,12 @@
<script lang="ts">
import {Attachment} from '@anticrm/attachment'
import { AttachmentPresenter } from '@anticrm/attachment-resources'
export let value: Attachment
// TODO: implement
</script>
<div>
<AttachmentPresenter {value} />
</div>

View File

@ -54,6 +54,7 @@ export default mergeIds(boardId, board, {
Checklist: '' as IntlString,
Dates: '' as IntlString,
Attachments: '' as IntlString,
AddAttachment: '' as IntlString,
CustomFields: '' as IntlString,
Automation: '' as IntlString,
AddButton: '' as IntlString,