Merge pull request from hcengineering/ano/attachments-initial

Board: Card Attachments
This commit is contained in:
Anna No 2022-04-15 10:51:42 +07:00 committed by GitHub
commit e8023deb93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 367 additions and 147 deletions

View File

@ -544,6 +544,7 @@ a.no-line {
}
/* Backgrounds & Colors */
.background-theme-content-accent { background-color: var(--theme-content-accent-color); }
.background-theme-bg-color { background-color: var(--theme-bg-color); }
.background-highlight-red { background-color: var(--highlight-red); }
.background-button-bg-color { background-color: var(--button-bg-color); }

View File

@ -0,0 +1,62 @@
<!--
// 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 { Class, Doc, Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { CircleButton, IconAdd } from '@anticrm/ui'
import { createAttachments } 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()
async function fileSelected() {
const list = inputFile.files
if (list === null || list.length === 0) return
loading++
try {
await createAttachments(client, list, { objectClass, objectId, space })
} finally {
loading--
}
}
function openFile() {
inputFile.click()
}
</script>
<div>
{#if $$slots.control}
<slot name="control" click={openFile} />
{: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,66 @@
<!--
// 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 { Class, Doc, Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { createAttachments } 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 canDrop: ((e: DragEvent) => boolean) | undefined = undefined
export let dragover = false
const client = getClient()
async function fileDrop(e: DragEvent) {
dragover = false
if (canDrop && !canDrop(e)) {
return
}
e.preventDefault()
e.stopPropagation()
const list = e.dataTransfer?.files
if (list === undefined || list.length === 0) return
loading++
try {
await createAttachments(client, list, { objectClass, objectId, space })
} finally {
loading--
}
}
</script>
<div
on:dragover={(e) => {
if (canDrop?.(e) ?? true) {
dragover = true
e.preventDefault()
}
}}
on:dragleave={() => {
dragover = false
}}
on:drop={fileDrop}
>
<slot />
</div>

View File

@ -15,63 +15,25 @@
-->
<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">
@ -80,54 +42,31 @@
{#if loading}
<Spinner />
{:else}
<CircleButton
icon={IconAdd}
size={'small'}
selected
on:click={() => {
inputFile.click()
}}
/>
<AddAttachment bind:loading bind:inputFile objectClass={_class} {objectId} {space} />
{/if}
<input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
</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 attachments === 0 && !loading}
<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" style:pointer-events="none">
<Label label={attachment.string.NoAttachments} />
</div>
<div class="text-sm" style:pointer-events={dragover ? 'none' : 'all'}>
<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 +89,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,11 +14,13 @@
// 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'
export async function uploadFile (file: File, opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }): Promise<string> {
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)
if (uploadUrl === undefined) {
@ -28,12 +30,16 @@ export async function uploadFile (file: File, opts?: { space: Ref<Space>, attach
const data = new FormData()
data.append('file', file)
const params = opts !== undefined
? [['space', opts.space], ['attachedTo', opts.attachedTo]]
.filter((x): x is [string, Ref<any>] => x[1] !== undefined)
.map(([name, value]) => `${name}=${value}`)
.join('&')
: ''
const params =
opts !== undefined
? [
['space', opts.space],
['attachedTo', opts.attachedTo]
]
.filter((x): x is [string, Ref<any>] => x[1] !== undefined)
.map(([name, value]) => `${name}=${value}`)
.join('&')
: ''
const suffix = params === '' ? params : `?${params}`
const url = `${uploadUrl}${suffix}`
@ -52,7 +58,7 @@ export async function uploadFile (file: File, opts?: { space: Ref<Space>, attach
return await resp.text()
}
export async function deleteFile (id: string): Promise<void> {
export async function deleteFile(id: string): Promise<void> {
const uploadUrl = getMetadata(login.metadata.UploadUrl)
const url = `${uploadUrl as string}?file=${id}`
@ -68,7 +74,33 @@ export async function deleteFile (id: string): Promise<void> {
}
}
export function getType (type: string): 'image' | 'video' | 'audio' | 'pdf' | 'other' {
export async function createAttachments(
client: Client,
list: FileList,
attachTo: { objectClass: Ref<Class<Doc>>; space: Ref<Space>; objectId: Ref<Doc> }
) {
const { objectClass, objectId, space } = attachTo
try {
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) {
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))
}
}
export function getType(type: string): 'image' | 'video' | 'audio' | 'pdf' | 'other' {
if (type.startsWith('image/')) {
return 'image'
}

View File

@ -33,6 +33,8 @@
"Checklist": "Checklist",
"Dates": "Dates",
"Attachments": "Attachments",
"AddAttachment": "Add an attachment",
"DropFileToUpload": "Drop files to upload.",
"CustomFields": "Custom Fields",
"Automation": "Automation",
"AddButton": "Add Button",

View File

@ -33,6 +33,8 @@
"Checklist": "Списки",
"Dates": "Дата",
"Attachments": "Прикрепленное",
"AddAttachment": "Прикрепить",
"DropFileToUpload": "Добавьте файлы.",
"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>
@ -123,7 +124,7 @@
/>
</div>
</div>
<!-- TODO attachments-->
<CardAttachments value={object} />
<!-- TODO checklists -->
<CardActivity bind:value={object} />
</div>

View File

@ -14,58 +14,82 @@
// 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'
import notification from '@anticrm/notification'
import { ActionIcon, Component, IconMoreH, showPanel, showPopup } from '@anticrm/ui'
import { ActionIcon, Component, IconMoreH, Label, showPanel, showPopup } from '@anticrm/ui'
import { ContextMenu } from '@anticrm/view-resources'
import board from '../plugin'
export let object: WithLookup<Card>
export let dragged: boolean
function showMenu(ev?: Event): void {
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')
}
function canDropAttachment (e: DragEvent): boolean {
return !!e.dataTransfer?.items && e.dataTransfer?.items.length > 0;
}
</script>
<div class="flex-col pt-2 pb-2 pr-4 pl-4">
<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}
canDrop={canDropAttachment}>
<div class="relative flex-col pt-2 pb-2 pr-4 pl-4">
{#if dragoverAttachment}
<div style:pointer-events="none" class="abs-full-content h-full w-full flex-center fs-title">
<Label label={board.string.DropFileToUpload} />
</div>
<div
style:opacity="0.3"
style:pointer-events="none"
class="abs-full-content background-theme-content-accent h-full w-full flex-center fs-title" />
{/if}
<div class="flex-between mb-4" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<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" style:pointer-events={dragoverAttachment ? 'none' : 'all'}>
<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>
</div>
</AttachmentDroppable>

View File

@ -27,15 +27,15 @@
export let value: Card
const client = getClient()
const suggestedActions: CardAction[] = []
const addToCardActions: CardAction[] = []
const automationActions: CardAction[] = []
const actions: CardAction[] = []
let actionGroups: { label: IntlString; actions: CardAction[] }[] = []
async function fetch() {
const suggestedActions: CardAction[] = []
const addToCardActions: CardAction[] = []
const automationActions: CardAction[] = []
const actions: CardAction[] = []
const result = await getCardActions(client)
for (const action of result) {
let supported = true
if (action.supported) {
@ -77,12 +77,7 @@
]
}
fetch()
$: value.members && fetch()
$: value.isArchived && fetch()
$: !value.isArchived && fetch()
$: fetch()
</script>
{#if value}
@ -107,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,65 @@
<!--
// 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 })
}
$: value?.attachments && value.attachments > 0 && 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}>
<svelte:fragment slot="control" let:click>
<Button label={board.string.AddAttachment} kind="no-border" on:click={click}/>
</svelte:fragment>
</AddAttachment>
</div>
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,26 @@
<!--
// 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 '@anticrm/attachment-resources'
export let value: Attachment
// TODO: implement
</script>
<div>
<AttachmentPresenter {value} />
</div>

View File

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