mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Chunter: FileBrowser - add grid view (#1571)
Signed-off-by: Ruslan Izhitsky <ruslan.izhitskiy@xored.com>
This commit is contained in:
parent
ed3d59044d
commit
e9a0225dd7
@ -17,6 +17,8 @@
|
||||
"Name": "Name",
|
||||
"FileBrowser": "File browser",
|
||||
"FileBrowserFileCounter": "{results, plural, =1 {# result} other {# results}}",
|
||||
"FileBrowserListView": "View as list",
|
||||
"FileBrowserGridView": "View as grid",
|
||||
"FileBrowserFilterFrom": "From",
|
||||
"FileBrowserFilterIn": "In",
|
||||
"FileBrowserFilterDate": "Date",
|
||||
|
@ -17,6 +17,8 @@
|
||||
"Name": "Название",
|
||||
"FileBrowser": "Браузер файлов",
|
||||
"FileBrowserFileCounter": "{results, plural, =1 {# результат} =2 {# результата} =3 {# результата} =4 {# результата} other {# результатов}}",
|
||||
"FileBrowserListView": "Показать в виде списка",
|
||||
"FileBrowserGridView": "Показать в виде таблицы",
|
||||
"FileBrowserFilterFrom": "От",
|
||||
"FileBrowserFilterIn": "В",
|
||||
"FileBrowserFilterDate": "Дата",
|
||||
|
@ -0,0 +1,200 @@
|
||||
<!--
|
||||
// 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 type { Attachment } from '@anticrm/attachment'
|
||||
import { showPopup, closeTooltip } from '@anticrm/ui'
|
||||
import { PDFViewer, getFileUrl } from '@anticrm/presentation'
|
||||
import filesize from 'filesize'
|
||||
|
||||
export let value: Attachment
|
||||
|
||||
const maxLength: number = 18
|
||||
const trimFilename = (fname: string): string =>
|
||||
fname.length > maxLength ? fname.substr(0, (maxLength - 1) / 2) + '...' + fname.substr(-(maxLength - 1) / 2) : fname
|
||||
|
||||
function extensionIconLabel (name: string): string {
|
||||
const parts = name.split('.')
|
||||
const ext = parts[parts.length - 1]
|
||||
return ext.substring(0, 4).toUpperCase()
|
||||
}
|
||||
|
||||
function isPDF (contentType: string) {
|
||||
return contentType.includes('application/pdf')
|
||||
}
|
||||
function isImage (contentType: string) {
|
||||
return contentType.startsWith('image/')
|
||||
}
|
||||
function isEmbedded (contentType: string) {
|
||||
return isPDF(contentType) || isImage(contentType)
|
||||
}
|
||||
|
||||
function openAttachment () {
|
||||
closeTooltip()
|
||||
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="gridCellOverlay">
|
||||
<div class="gridCell">
|
||||
{#if isImage(value.type)}
|
||||
<div class="cellImagePreview" on:click={openAttachment}>
|
||||
<img class={'img-fit'} src={getFileUrl(value.file)} alt={value.name} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="cellMiscPreview">
|
||||
{#if isPDF(value.type)}
|
||||
<div class="flex-center extensionIcon" on:click={openAttachment}>
|
||||
{extensionIconLabel(value.name)}
|
||||
</div>
|
||||
{:else}
|
||||
<a class="no-line" href={getFileUrl(value.file)} download={value.name}>
|
||||
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="cellInfo">
|
||||
{#if isEmbedded(value.type)}
|
||||
<div class="flex-center extensionIcon" on:click={openAttachment}>
|
||||
{extensionIconLabel(value.name)}
|
||||
</div>
|
||||
{:else}
|
||||
<a class="no-line" href={getFileUrl(value.file)} download={value.name}>
|
||||
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
|
||||
</a>
|
||||
{/if}
|
||||
<div class="eCellInfoData">
|
||||
{#if isEmbedded(value.type)}
|
||||
<div class="eCellInfoFilename" on:click={openAttachment}>
|
||||
{trimFilename(value.name)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="eCellInfoFilename">
|
||||
<a href={getFileUrl(value.file)} download={value.name}>{trimFilename(value.name)}</a>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
|
||||
</div>
|
||||
<div class="eCellInfoMenu"><slot name="rowMenu" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.gridCellOverlay {
|
||||
position: relative;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.gridCell {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px var(--theme-bg-focused-border);
|
||||
}
|
||||
|
||||
.cellImagePreview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
margin: 0 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--theme-menu-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cellMiscPreview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.img-fit {
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cellInfo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 12px;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
|
||||
.eCellInfoData {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.eCellInfoMenu {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.eCellInfoFilename {
|
||||
font-weight: 500;
|
||||
color: var(--theme-content-accent-color);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eCellInfoFilesize {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
}
|
||||
|
||||
.extensionIcon {
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.625rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eCellInfoFilename:hover,
|
||||
.extensionIcon:hover + .eCellInfoData > .eCellInfoFilename, // embedded on extension hover
|
||||
.no-line:hover + .eCellInfoData > .eCellInfoFilename a, // not embedded on extension hover
|
||||
.cellImagePreview:hover + .cellInfo .eCellInfoFilename, // image on preview hover
|
||||
.cellMiscPreview:hover + .cellInfo .eCellInfoFilename, // PDF on preview hover
|
||||
.cellMiscPreview:hover + .cellInfo .eCellInfoFilename a // not embedded on preview hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.eCellInfoFilename:active,
|
||||
.extensionIcon:active + .eCellInfoData > .eCellInfoFilename, // embedded on extension hover
|
||||
.no-line:active + .eCellInfoData > .eCellInfoFilename a, // not embedded on extension hover
|
||||
.cellImagePreview:active + .cellInfo .eCellInfoFilename, // image on preview hover
|
||||
.cellMiscPreview:active + .cellInfo .eCellInfoFilename, // PDF on preview hover
|
||||
.cellMiscPreview:active + .cellInfo .eCellInfoFilename a // not embedded on preview hover
|
||||
{
|
||||
text-decoration: underline;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
</style>
|
@ -13,7 +13,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@anticrm/attachment'
|
||||
import { showPopup, closeTooltip } from '@anticrm/ui'
|
||||
@ -23,9 +22,8 @@
|
||||
export let value: Attachment
|
||||
|
||||
const maxLenght: number = 16
|
||||
const trimFilename = (fname: string): string => (fname.length > maxLenght)
|
||||
? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2)
|
||||
: fname
|
||||
const trimFilename = (fname: string): string =>
|
||||
fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname
|
||||
|
||||
function iconLabel (name: string): string {
|
||||
const parts = name.split('.')
|
||||
@ -40,18 +38,33 @@
|
||||
|
||||
<div class="flex-row-center">
|
||||
{#if openEmbedded(value.type)}
|
||||
<div class="flex-center icon" on:click={() => {
|
||||
closeTooltip()
|
||||
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
|
||||
}}>{iconLabel(value.name)}</div>
|
||||
<div
|
||||
class="flex-center icon"
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
|
||||
}}
|
||||
>
|
||||
{iconLabel(value.name)}
|
||||
</div>
|
||||
{:else}
|
||||
<a class="no-line" href={getFileUrl(value.file)} download={value.name}><div class="flex-center icon">{iconLabel(value.name)}</div></a>
|
||||
<a class="no-line" href={getFileUrl(value.file)} download={value.name}
|
||||
><div class="flex-center icon">{iconLabel(value.name)}</div></a
|
||||
>
|
||||
{/if}
|
||||
<div class="flex-col info">
|
||||
{#if openEmbedded(value.type)}
|
||||
<div class="name" on:click={() => { closeTooltip(); showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right') }}>{trimFilename(value.name)}</div>
|
||||
<div
|
||||
class="name"
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
|
||||
}}
|
||||
>
|
||||
{trimFilename(value.name)}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="name"><a href={getFileUrl(value.file)} download={value.name}>{trimFilename(value.name)}</a></div>
|
||||
<div class="name"><a href={getFileUrl(value.file)} download={value.name}>{trimFilename(value.name)}</a></div>
|
||||
{/if}
|
||||
<div class="type">{filesize(value.size)}</div>
|
||||
</div>
|
||||
@ -64,11 +77,11 @@
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-weight: 500;
|
||||
font-size: .625rem;
|
||||
font-size: 0.625rem;
|
||||
color: var(--primary-button-color);
|
||||
background-color: var(--primary-button-enabled);
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -80,15 +93,19 @@
|
||||
}
|
||||
|
||||
.type {
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
|
||||
.name:hover, .icon:hover + .info > .name, .no-line:hover + .info > .name a {
|
||||
.name:hover,
|
||||
.icon:hover + .info > .name,
|
||||
.no-line:hover + .info > .name a {
|
||||
text-decoration: underline;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.name:active, .icon:active + .info > .name, .no-line:active + .info > .name a {
|
||||
.name:active,
|
||||
.icon:active + .info > .name,
|
||||
.no-line:active + .info > .name a {
|
||||
text-decoration: underline;
|
||||
color: var(--theme-content-accent-color);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
@ -44,15 +43,20 @@
|
||||
const query = createQuery()
|
||||
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
|
||||
let originalAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
|
||||
let newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
|
||||
let removedAttachments: Set<Attachment> = new Set<Attachment>()
|
||||
const newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
|
||||
const removedAttachments: Set<Attachment> = new Set<Attachment>()
|
||||
|
||||
$: objectId && query.query(attachment.class.Attachment, {
|
||||
attachedTo: objectId
|
||||
}, (res) => {
|
||||
originalAttachments = new Set(res.map((p) => p._id))
|
||||
attachments = new Map(res.map((p) => [p._id, p]))
|
||||
})
|
||||
$: objectId &&
|
||||
query.query(
|
||||
attachment.class.Attachment,
|
||||
{
|
||||
attachedTo: objectId
|
||||
},
|
||||
(res) => {
|
||||
originalAttachments = new Set(res.map((p) => p._id))
|
||||
attachments = new Map(res.map((p) => [p._id, p]))
|
||||
}
|
||||
)
|
||||
|
||||
async function createAttachment (file: File) {
|
||||
try {
|
||||
@ -82,7 +86,15 @@
|
||||
}
|
||||
|
||||
async function saveAttachment (doc: Attachment) {
|
||||
const res = await client.addCollection(attachment.class.Attachment, space, objectId, _class, 'attachments', doc, doc._id)
|
||||
const res = await client.addCollection(
|
||||
attachment.class.Attachment,
|
||||
space,
|
||||
objectId,
|
||||
_class,
|
||||
'attachments',
|
||||
doc,
|
||||
doc._id
|
||||
)
|
||||
}
|
||||
|
||||
function fileSelected () {
|
||||
@ -112,7 +124,14 @@
|
||||
|
||||
async function deleteAttachment (attachment: Attachment): Promise<void> {
|
||||
if (originalAttachments.has(attachment._id)) {
|
||||
await client.removeCollection(attachment._class, attachment.space, attachment._id, attachment.attachedTo, attachment.attachedToClass, 'attachments')
|
||||
await client.removeCollection(
|
||||
attachment._class,
|
||||
attachment.space,
|
||||
attachment._id,
|
||||
attachment.attachedTo,
|
||||
attachment.attachedToClass,
|
||||
'attachments'
|
||||
)
|
||||
} else {
|
||||
await deleteFile(attachment.file)
|
||||
}
|
||||
@ -144,7 +163,6 @@
|
||||
await Promise.all(promises)
|
||||
dispatch('message', { message: event.detail, attachments: attachments.size })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<input
|
||||
@ -155,25 +173,41 @@
|
||||
id="file"
|
||||
style="display: none"
|
||||
on:change={fileSelected}
|
||||
/>
|
||||
<div class="container"
|
||||
/>
|
||||
<div
|
||||
class="container"
|
||||
on:dragover|preventDefault={() => {}}
|
||||
on:dragleave={() => {}}
|
||||
on:drop|preventDefault|stopPropagation={fileDrop}
|
||||
>
|
||||
>
|
||||
{#if attachments.size}
|
||||
<div class='flex-row-center list'>
|
||||
<div class="flex-row-center list">
|
||||
{#each Array.from(attachments.values()) as attachment}
|
||||
<div class='item flex'>
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter value={attachment} />
|
||||
<div class='remove'>
|
||||
<ActionIcon icon={IconClose} action={() => { removeAttachment(attachment) }} size='small' />
|
||||
<div class="remove">
|
||||
<ActionIcon
|
||||
icon={IconClose}
|
||||
action={() => {
|
||||
removeAttachment(attachment)
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<ReferenceInput bind:this={refInput} {content} {showSend} on:message={onMessage} withoutTopBorder={attachments.size > 0} on:attach={() => { inputFile.click() }} />
|
||||
<ReferenceInput
|
||||
bind:this={refInput}
|
||||
{content}
|
||||
{showSend}
|
||||
on:message={onMessage}
|
||||
withoutTopBorder={attachments.size > 0}
|
||||
on:attach={() => {
|
||||
inputFile.click()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@ -183,7 +217,7 @@
|
||||
overflow-x: auto;
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
border-radius: 0.75rem;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
|
@ -0,0 +1,98 @@
|
||||
<!--
|
||||
// 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 { Doc } from '@anticrm/core'
|
||||
import { getFileUrl } from '@anticrm/presentation'
|
||||
import { Icon, IconMoreV, showPopup } from '@anticrm/ui'
|
||||
import { Menu } from '@anticrm/view-resources'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import { AttachmentGalleryPresenter } from '..'
|
||||
|
||||
export let attachments: Attachment[]
|
||||
let selectedFileNumber: number | undefined
|
||||
|
||||
const showFileMenu = async (ev: MouseEvent, object: Doc, fileNumber: number): Promise<void> => {
|
||||
selectedFileNumber = fileNumber
|
||||
showPopup(Menu, { object }, ev.target as HTMLElement, () => {
|
||||
selectedFileNumber = undefined
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="galleryGrid">
|
||||
{#each attachments as attachment, i}
|
||||
<div class="attachmentCell" class:fixed={i === selectedFileNumber}>
|
||||
<AttachmentGalleryPresenter value={attachment}>
|
||||
<svelte:fragment slot="rowMenu">
|
||||
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
|
||||
<a href={getFileUrl(attachment.file)} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</AttachmentGalleryPresenter>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.galleryGrid {
|
||||
display: grid;
|
||||
margin: 0 1.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.attachmentCell {
|
||||
.eAttachmentCellActions {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
border: 1px solid var(--theme-bg-focused-border);
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.eAttachmentCellMenu {
|
||||
visibility: hidden;
|
||||
margin-left: 0.2rem;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.eAttachmentCellActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentCellMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.fixed {
|
||||
.eAttachmentCellActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentCellMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<!--
|
||||
// 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 { Doc } from '@anticrm/core'
|
||||
import { getFileUrl } from '@anticrm/presentation'
|
||||
import { Icon, IconMoreV, showPopup } from '@anticrm/ui'
|
||||
import { Menu } from '@anticrm/view-resources'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import { AttachmentPresenter } from '..'
|
||||
|
||||
export let attachments: Attachment[]
|
||||
let selectedFileNumber: number | undefined
|
||||
|
||||
const showFileMenu = async (ev: MouseEvent, object: Doc, fileNumber: number): Promise<void> => {
|
||||
selectedFileNumber = fileNumber
|
||||
showPopup(Menu, { object }, ev.target as HTMLElement, () => {
|
||||
selectedFileNumber = undefined
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col">
|
||||
{#each attachments as attachment, i}
|
||||
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
|
||||
<a href={getFileUrl(attachment.file)} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<div class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.attachmentRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 1.5rem;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
.eAttachmentRowActions {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
border: 1px solid var(--theme-bg-focused-border);
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.eAttachmentRowMenu {
|
||||
visibility: hidden;
|
||||
margin-left: 0.2rem;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.eAttachmentRowActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentRowMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.fixed {
|
||||
.eAttachmentRowActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentRowMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -16,31 +16,24 @@
|
||||
import { Attachment } from '@anticrm/attachment'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { EmployeeAccount } from '@anticrm/contact'
|
||||
import core, { Class, Doc, getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import { getClient, getFileUrl } from '@anticrm/presentation'
|
||||
import core, { Class, getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import ui, {
|
||||
getCurrentLocation,
|
||||
location,
|
||||
IconMoreV,
|
||||
IconSearch,
|
||||
Label,
|
||||
showPopup,
|
||||
navigate,
|
||||
EditWithIcon,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
Icon
|
||||
} from '@anticrm/ui'
|
||||
import { Menu } from '@anticrm/view-resources'
|
||||
import { onDestroy } from 'svelte'
|
||||
import {
|
||||
AttachmentPresenter,
|
||||
FileBrowserSortMode,
|
||||
dateFileBrowserFilters,
|
||||
fileTypeFileBrowserFilters,
|
||||
sortModeToOptionObject
|
||||
} from '..'
|
||||
import { FileBrowserSortMode, dateFileBrowserFilters, fileTypeFileBrowserFilters, sortModeToOptionObject } from '..'
|
||||
import attachment from '../plugin'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import AttachmentsGalleryView from './AttachmentsGalleryView.svelte'
|
||||
import AttachmentsListView from './AttachmentsListView.svelte'
|
||||
import FileBrowserFilters from './FileBrowserFilters.svelte'
|
||||
import FileBrowserSortMenu from './FileBrowserSortMenu.svelte'
|
||||
|
||||
@ -55,22 +48,15 @@
|
||||
let isLoading = false
|
||||
|
||||
let attachments: Attachment[] = []
|
||||
let selectedFileNumber: number | undefined
|
||||
|
||||
let selectedSort: FileBrowserSortMode = FileBrowserSortMode.NewestFile
|
||||
let selectedDateId = 'dateAny'
|
||||
let selectedFileTypeId = 'typeAny'
|
||||
|
||||
const showFileMenu = async (ev: MouseEvent, object: Doc, fileNumber: number): Promise<void> => {
|
||||
selectedFileNumber = fileNumber
|
||||
showPopup(Menu, { object }, ev.target as HTMLElement, () => {
|
||||
selectedFileNumber = undefined
|
||||
})
|
||||
}
|
||||
let isListDisplayMode = true
|
||||
|
||||
$: fetch(searchQuery, selectedSort, selectedFileTypeId, selectedDateId, selectedParticipants, selectedSpaces)
|
||||
|
||||
async function fetch(
|
||||
async function fetch (
|
||||
searchQuery_: string,
|
||||
selectedSort_: FileBrowserSortMode,
|
||||
selectedFileTypeId_: string,
|
||||
@ -131,14 +117,40 @@
|
||||
</div>
|
||||
<EditWithIcon icon={IconSearch} bind:value={searchQuery} placeholder={ui.string.SearchDots} />
|
||||
</div>
|
||||
<FileBrowserFilters
|
||||
{requestedSpaceClasses}
|
||||
{spaceId}
|
||||
bind:selectedParticipants
|
||||
bind:selectedSpaces
|
||||
bind:selectedDateId
|
||||
bind:selectedFileTypeId
|
||||
/>
|
||||
<div class="ac-header full">
|
||||
<FileBrowserFilters
|
||||
{requestedSpaceClasses}
|
||||
{spaceId}
|
||||
bind:selectedParticipants
|
||||
bind:selectedSpaces
|
||||
bind:selectedDateId
|
||||
bind:selectedFileTypeId
|
||||
/>
|
||||
<div class="flex">
|
||||
<Tooltip label={attachment.string.FileBrowserListView} direction={'bottom'}>
|
||||
<button
|
||||
class="ac-header__icon-button"
|
||||
class:selected={isListDisplayMode}
|
||||
on:click={() => {
|
||||
isListDisplayMode = true
|
||||
}}
|
||||
>
|
||||
<Icon icon={contact.icon.Person} size={'small'} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label={attachment.string.FileBrowserGridView} direction={'bottom'}>
|
||||
<button
|
||||
class="ac-header__icon-button"
|
||||
class:selected={!isListDisplayMode}
|
||||
on:click={() => {
|
||||
isListDisplayMode = false
|
||||
}}
|
||||
>
|
||||
<Icon icon={contact.icon.Edit} size={'small'} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="groupHeader">
|
||||
<div class="eGroupHeaderCount">
|
||||
@ -151,23 +163,11 @@
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if attachments?.length}
|
||||
<div class="flex-col">
|
||||
{#each attachments as attachment, i}
|
||||
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
|
||||
<div class="item flex">
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
|
||||
<a href={getFileUrl(attachment.file)} download={attachment.name}>
|
||||
<Icon icon={FileDownload} size={'small'} />
|
||||
</a>
|
||||
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
<IconMoreV size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if isListDisplayMode}
|
||||
<AttachmentsListView {attachments} />
|
||||
{:else}
|
||||
<AttachmentsGalleryView {attachments} />
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-between attachmentRow">
|
||||
<Label label={attachment.string.NoFiles} />
|
||||
@ -191,48 +191,4 @@
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.attachmentRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 1rem;
|
||||
margin: 0 1.5rem;
|
||||
padding: 0.25rem 0;
|
||||
|
||||
.eAttachmentRowActions {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
border: 1px solid var(--theme-bg-focused-border);
|
||||
padding: 0.2rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.eAttachmentRowMenu {
|
||||
margin-left: 0.2rem;
|
||||
visibility: hidden;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.eAttachmentRowActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentRowMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.fixed {
|
||||
.eAttachmentRowActions {
|
||||
visibility: visible;
|
||||
}
|
||||
.eAttachmentRowMenu {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,6 +17,7 @@ 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 AttachmentGalleryPresenter from './components/AttachmentGalleryPresenter.svelte'
|
||||
import AttachmentDocList from './components/AttachmentDocList.svelte'
|
||||
import AttachmentList from './components/AttachmentList.svelte'
|
||||
import AttachmentRefInput from './components/AttachmentRefInput.svelte'
|
||||
@ -27,7 +28,7 @@ import Photos from './components/Photos.svelte'
|
||||
import FileDownload from './components/icons/FileDownload.svelte'
|
||||
import { uploadFile, deleteFile } from './utils'
|
||||
import attachment, { Attachment } from '@anticrm/attachment'
|
||||
import { SortingOrder, SortingQuery } from '@anticrm/core'
|
||||
import { ObjQueryType, SortingOrder, SortingQuery } from '@anticrm/core'
|
||||
import { IntlString, Resources } from '@anticrm/platform'
|
||||
import preference from '@anticrm/preference'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
@ -38,6 +39,7 @@ export {
|
||||
Attachments,
|
||||
AttachmentsPresenter,
|
||||
AttachmentPresenter,
|
||||
AttachmentGalleryPresenter,
|
||||
AttachmentRefInput,
|
||||
AttachmentList,
|
||||
AttachmentDocList,
|
||||
@ -72,18 +74,27 @@ export const sortModeToOptionObject = (sortMode: FileBrowserSortMode): SortingQu
|
||||
|
||||
const msInDay = 24 * 60 * 60 * 1000
|
||||
const getBeginningOfDate = (customDate?: Date) => {
|
||||
if (!customDate) {
|
||||
if (customDate == null) {
|
||||
customDate = new Date()
|
||||
}
|
||||
customDate.setUTCHours(0, 0, 0, 0)
|
||||
return customDate.getTime()
|
||||
}
|
||||
|
||||
export const dateFileBrowserFilters: {
|
||||
interface Filter {
|
||||
id: string
|
||||
label: IntlString<{}>
|
||||
getDate: () => any
|
||||
}[] = [
|
||||
label: IntlString
|
||||
}
|
||||
|
||||
interface DateFilter extends Filter {
|
||||
getDate: () => ObjQueryType<number> | undefined
|
||||
}
|
||||
|
||||
interface TypeFilter extends Filter {
|
||||
getType: () => ObjQueryType<string> | undefined
|
||||
}
|
||||
|
||||
export const dateFileBrowserFilters: DateFilter[] = [
|
||||
{
|
||||
id: 'dateAny',
|
||||
label: attachment.string.FileBrowserDateFilterAny,
|
||||
@ -139,11 +150,7 @@ export const dateFileBrowserFilters: {
|
||||
}
|
||||
]
|
||||
|
||||
export const fileTypeFileBrowserFilters: {
|
||||
id: string
|
||||
label: IntlString<{}>
|
||||
getType: () => any
|
||||
}[] = [
|
||||
export const fileTypeFileBrowserFilters: TypeFilter[] = [
|
||||
{
|
||||
id: 'typeAny',
|
||||
label: attachment.string.FileBrowserTypeFilterAny,
|
||||
@ -181,7 +188,7 @@ export const fileTypeFileBrowserFilters: {
|
||||
}
|
||||
]
|
||||
|
||||
export async function AddAttachmentToSaved(attach: Attachment): Promise<void> {
|
||||
export async function AddAttachmentToSaved (attach: Attachment): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
await client.createDoc(attachment.class.SavedAttachments, preference.space.Preference, {
|
||||
@ -189,7 +196,7 @@ export async function AddAttachmentToSaved(attach: Attachment): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function DeleteAttachmentFromSaved(attach: Attachment): Promise<void> {
|
||||
export async function DeleteAttachmentFromSaved (attach: Attachment): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
const current = await client.findOne(attachment.class.SavedAttachments, { attachedTo: attach._id })
|
||||
@ -202,6 +209,7 @@ export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
AttachmentsPresenter,
|
||||
AttachmentPresenter,
|
||||
AttachmentGalleryPresenter,
|
||||
Attachments,
|
||||
FileBrowser,
|
||||
Photos
|
||||
|
@ -28,6 +28,8 @@ export default mergeIds(attachmentId, attachment, {
|
||||
Photos: '' as IntlString,
|
||||
FileBrowser: '' as IntlString,
|
||||
FileBrowserFileCounter: '' as IntlString,
|
||||
FileBrowserListView: '' as IntlString,
|
||||
FileBrowserGridView: '' as IntlString,
|
||||
FileBrowserFilterFrom: '' as IntlString,
|
||||
FileBrowserFilterIn: '' as IntlString,
|
||||
FileBrowserFilterDate: '' as IntlString,
|
||||
|
@ -229,7 +229,7 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: .5rem 2rem;
|
||||
padding: 0.5rem 2rem;
|
||||
|
||||
.avatar {
|
||||
min-width: 2.25rem;
|
||||
|
Loading…
Reference in New Issue
Block a user