Chunter: FileBrowser - add grid view (#1571)

Signed-off-by: Ruslan Izhitsky <ruslan.izhitskiy@xored.com>
This commit is contained in:
Ruslan Izhitsky 2022-04-28 13:45:07 +07:00 committed by GitHub
parent ed3d59044d
commit e9a0225dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 558 additions and 143 deletions

View File

@ -17,6 +17,8 @@
"Name": "Name", "Name": "Name",
"FileBrowser": "File browser", "FileBrowser": "File browser",
"FileBrowserFileCounter": "{results, plural, =1 {# result} other {# results}}", "FileBrowserFileCounter": "{results, plural, =1 {# result} other {# results}}",
"FileBrowserListView": "View as list",
"FileBrowserGridView": "View as grid",
"FileBrowserFilterFrom": "From", "FileBrowserFilterFrom": "From",
"FileBrowserFilterIn": "In", "FileBrowserFilterIn": "In",
"FileBrowserFilterDate": "Date", "FileBrowserFilterDate": "Date",

View File

@ -17,6 +17,8 @@
"Name": "Название", "Name": "Название",
"FileBrowser": "Браузер файлов", "FileBrowser": "Браузер файлов",
"FileBrowserFileCounter": "{results, plural, =1 {# результат} =2 {# результата} =3 {# результата} =4 {# результата} other {# результатов}}", "FileBrowserFileCounter": "{results, plural, =1 {# результат} =2 {# результата} =3 {# результата} =4 {# результата} other {# результатов}}",
"FileBrowserListView": "Показать в виде списка",
"FileBrowserGridView": "Показать в виде таблицы",
"FileBrowserFilterFrom": "От", "FileBrowserFilterFrom": "От",
"FileBrowserFilterIn": "В", "FileBrowserFilterIn": "В",
"FileBrowserFilterDate": "Дата", "FileBrowserFilterDate": "Дата",

View File

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

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@anticrm/attachment' import type { Attachment } from '@anticrm/attachment'
import { showPopup, closeTooltip } from '@anticrm/ui' import { showPopup, closeTooltip } from '@anticrm/ui'
@ -23,9 +22,8 @@
export let value: Attachment export let value: Attachment
const maxLenght: number = 16 const maxLenght: number = 16
const trimFilename = (fname: string): string => (fname.length > maxLenght) const trimFilename = (fname: string): string =>
? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) fname.length > maxLenght ? fname.substr(0, (maxLenght - 1) / 2) + '...' + fname.substr(-(maxLenght - 1) / 2) : fname
: fname
function iconLabel (name: string): string { function iconLabel (name: string): string {
const parts = name.split('.') const parts = name.split('.')
@ -40,18 +38,33 @@
<div class="flex-row-center"> <div class="flex-row-center">
{#if openEmbedded(value.type)} {#if openEmbedded(value.type)}
<div class="flex-center icon" on:click={() => { <div
closeTooltip() class="flex-center icon"
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right') on:click={() => {
}}>{iconLabel(value.name)}</div> closeTooltip()
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
}}
>
{iconLabel(value.name)}
</div>
{:else} {: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} {/if}
<div class="flex-col info"> <div class="flex-col info">
{#if openEmbedded(value.type)} {#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} {: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} {/if}
<div class="type">{filesize(value.size)}</div> <div class="type">{filesize(value.size)}</div>
</div> </div>
@ -64,11 +77,11 @@
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
font-weight: 500; font-weight: 500;
font-size: .625rem; font-size: 0.625rem;
color: var(--primary-button-color); color: var(--primary-button-color);
background-color: var(--primary-button-enabled); background-color: var(--primary-button-enabled);
border: 1px solid rgba(0, 0, 0, .1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: .5rem; border-radius: 0.5rem;
cursor: pointer; cursor: pointer;
} }
@ -80,15 +93,19 @@
} }
.type { .type {
font-size: .75rem; font-size: 0.75rem;
color: var(--theme-content-dark-color); 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; text-decoration: underline;
color: var(--theme-caption-color); 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; text-decoration: underline;
color: var(--theme-content-accent-color); color: var(--theme-content-accent-color);
} }

View File

@ -1,4 +1,3 @@
<!-- <!--
// Copyright © 2022 Hardcore Engineering Inc. // Copyright © 2022 Hardcore Engineering Inc.
// //
@ -44,15 +43,20 @@
const query = createQuery() const query = createQuery()
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>() let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
let originalAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>() let originalAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
let newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>() const newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
let removedAttachments: Set<Attachment> = new Set<Attachment>() const removedAttachments: Set<Attachment> = new Set<Attachment>()
$: objectId && query.query(attachment.class.Attachment, { $: objectId &&
attachedTo: objectId query.query(
}, (res) => { attachment.class.Attachment,
originalAttachments = new Set(res.map((p) => p._id)) {
attachments = new Map(res.map((p) => [p._id, p])) 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) { async function createAttachment (file: File) {
try { try {
@ -82,7 +86,15 @@
} }
async function saveAttachment (doc: Attachment) { 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 () { function fileSelected () {
@ -112,7 +124,14 @@
async function deleteAttachment (attachment: Attachment): Promise<void> { async function deleteAttachment (attachment: Attachment): Promise<void> {
if (originalAttachments.has(attachment._id)) { 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 { } else {
await deleteFile(attachment.file) await deleteFile(attachment.file)
} }
@ -144,7 +163,6 @@
await Promise.all(promises) await Promise.all(promises)
dispatch('message', { message: event.detail, attachments: attachments.size }) dispatch('message', { message: event.detail, attachments: attachments.size })
} }
</script> </script>
<input <input
@ -155,25 +173,41 @@
id="file" id="file"
style="display: none" style="display: none"
on:change={fileSelected} on:change={fileSelected}
/> />
<div class="container" <div
class="container"
on:dragover|preventDefault={() => {}} on:dragover|preventDefault={() => {}}
on:dragleave={() => {}} on:dragleave={() => {}}
on:drop|preventDefault|stopPropagation={fileDrop} on:drop|preventDefault|stopPropagation={fileDrop}
> >
{#if attachments.size} {#if attachments.size}
<div class='flex-row-center list'> <div class="flex-row-center list">
{#each Array.from(attachments.values()) as attachment} {#each Array.from(attachments.values()) as attachment}
<div class='item flex'> <div class="item flex">
<AttachmentPresenter value={attachment} /> <AttachmentPresenter value={attachment} />
<div class='remove'> <div class="remove">
<ActionIcon icon={IconClose} action={() => { removeAttachment(attachment) }} size='small' /> <ActionIcon
icon={IconClose}
action={() => {
removeAttachment(attachment)
}}
size="small"
/>
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
{/if} {/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> </div>
<style lang="scss"> <style lang="scss">
@ -183,7 +217,7 @@
overflow-x: auto; overflow-x: auto;
background-color: var(--theme-bg-accent-color); background-color: var(--theme-bg-accent-color);
border: 1px solid 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-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;

View File

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

View File

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

View File

@ -16,31 +16,24 @@
import { Attachment } from '@anticrm/attachment' import { Attachment } from '@anticrm/attachment'
import contact, { Employee } from '@anticrm/contact' import contact, { Employee } from '@anticrm/contact'
import { EmployeeAccount } from '@anticrm/contact' import { EmployeeAccount } from '@anticrm/contact'
import core, { Class, Doc, getCurrentAccount, Ref, Space } from '@anticrm/core' import core, { Class, getCurrentAccount, Ref, Space } from '@anticrm/core'
import { getClient, getFileUrl } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import ui, { import ui, {
getCurrentLocation, getCurrentLocation,
location, location,
IconMoreV,
IconSearch, IconSearch,
Label, Label,
showPopup,
navigate, navigate,
EditWithIcon, EditWithIcon,
Spinner, Spinner,
Tooltip,
Icon Icon
} from '@anticrm/ui' } from '@anticrm/ui'
import { Menu } from '@anticrm/view-resources'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { import { FileBrowserSortMode, dateFileBrowserFilters, fileTypeFileBrowserFilters, sortModeToOptionObject } from '..'
AttachmentPresenter,
FileBrowserSortMode,
dateFileBrowserFilters,
fileTypeFileBrowserFilters,
sortModeToOptionObject
} from '..'
import attachment from '../plugin' 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 FileBrowserFilters from './FileBrowserFilters.svelte'
import FileBrowserSortMenu from './FileBrowserSortMenu.svelte' import FileBrowserSortMenu from './FileBrowserSortMenu.svelte'
@ -55,22 +48,15 @@
let isLoading = false let isLoading = false
let attachments: Attachment[] = [] let attachments: Attachment[] = []
let selectedFileNumber: number | undefined
let selectedSort: FileBrowserSortMode = FileBrowserSortMode.NewestFile let selectedSort: FileBrowserSortMode = FileBrowserSortMode.NewestFile
let selectedDateId = 'dateAny' let selectedDateId = 'dateAny'
let selectedFileTypeId = 'typeAny' let selectedFileTypeId = 'typeAny'
let isListDisplayMode = true
const showFileMenu = async (ev: MouseEvent, object: Doc, fileNumber: number): Promise<void> => {
selectedFileNumber = fileNumber
showPopup(Menu, { object }, ev.target as HTMLElement, () => {
selectedFileNumber = undefined
})
}
$: fetch(searchQuery, selectedSort, selectedFileTypeId, selectedDateId, selectedParticipants, selectedSpaces) $: fetch(searchQuery, selectedSort, selectedFileTypeId, selectedDateId, selectedParticipants, selectedSpaces)
async function fetch( async function fetch (
searchQuery_: string, searchQuery_: string,
selectedSort_: FileBrowserSortMode, selectedSort_: FileBrowserSortMode,
selectedFileTypeId_: string, selectedFileTypeId_: string,
@ -131,14 +117,40 @@
</div> </div>
<EditWithIcon icon={IconSearch} bind:value={searchQuery} placeholder={ui.string.SearchDots} /> <EditWithIcon icon={IconSearch} bind:value={searchQuery} placeholder={ui.string.SearchDots} />
</div> </div>
<FileBrowserFilters <div class="ac-header full">
{requestedSpaceClasses} <FileBrowserFilters
{spaceId} {requestedSpaceClasses}
bind:selectedParticipants {spaceId}
bind:selectedSpaces bind:selectedParticipants
bind:selectedDateId bind:selectedSpaces
bind:selectedFileTypeId 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="group">
<div class="groupHeader"> <div class="groupHeader">
<div class="eGroupHeaderCount"> <div class="eGroupHeaderCount">
@ -151,23 +163,11 @@
<Spinner /> <Spinner />
</div> </div>
{:else if attachments?.length} {:else if attachments?.length}
<div class="flex-col"> {#if isListDisplayMode}
{#each attachments as attachment, i} <AttachmentsListView {attachments} />
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}> {:else}
<div class="item flex"> <AttachmentsGalleryView {attachments} />
<AttachmentPresenter value={attachment} /> {/if}
</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>
{:else} {:else}
<div class="flex-between attachmentRow"> <div class="flex-between attachmentRow">
<Label label={attachment.string.NoFiles} /> <Label label={attachment.string.NoFiles} />
@ -191,48 +191,4 @@
color: var(--theme-caption-color); 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> </style>

View File

@ -17,6 +17,7 @@ import AddAttachment from './components/AddAttachment.svelte'
import AttachmentDroppable from './components/AttachmentDroppable.svelte' import AttachmentDroppable from './components/AttachmentDroppable.svelte'
import AttachmentsPresenter from './components/AttachmentsPresenter.svelte' import AttachmentsPresenter from './components/AttachmentsPresenter.svelte'
import AttachmentPresenter from './components/AttachmentPresenter.svelte' import AttachmentPresenter from './components/AttachmentPresenter.svelte'
import AttachmentGalleryPresenter from './components/AttachmentGalleryPresenter.svelte'
import AttachmentDocList from './components/AttachmentDocList.svelte' import AttachmentDocList from './components/AttachmentDocList.svelte'
import AttachmentList from './components/AttachmentList.svelte' import AttachmentList from './components/AttachmentList.svelte'
import AttachmentRefInput from './components/AttachmentRefInput.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 FileDownload from './components/icons/FileDownload.svelte'
import { uploadFile, deleteFile } from './utils' import { uploadFile, deleteFile } from './utils'
import attachment, { Attachment } from '@anticrm/attachment' 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 { IntlString, Resources } from '@anticrm/platform'
import preference from '@anticrm/preference' import preference from '@anticrm/preference'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
@ -38,6 +39,7 @@ export {
Attachments, Attachments,
AttachmentsPresenter, AttachmentsPresenter,
AttachmentPresenter, AttachmentPresenter,
AttachmentGalleryPresenter,
AttachmentRefInput, AttachmentRefInput,
AttachmentList, AttachmentList,
AttachmentDocList, AttachmentDocList,
@ -72,18 +74,27 @@ export const sortModeToOptionObject = (sortMode: FileBrowserSortMode): SortingQu
const msInDay = 24 * 60 * 60 * 1000 const msInDay = 24 * 60 * 60 * 1000
const getBeginningOfDate = (customDate?: Date) => { const getBeginningOfDate = (customDate?: Date) => {
if (!customDate) { if (customDate == null) {
customDate = new Date() customDate = new Date()
} }
customDate.setUTCHours(0, 0, 0, 0) customDate.setUTCHours(0, 0, 0, 0)
return customDate.getTime() return customDate.getTime()
} }
export const dateFileBrowserFilters: { interface Filter {
id: string id: string
label: IntlString<{}> label: IntlString
getDate: () => any }
}[] = [
interface DateFilter extends Filter {
getDate: () => ObjQueryType<number> | undefined
}
interface TypeFilter extends Filter {
getType: () => ObjQueryType<string> | undefined
}
export const dateFileBrowserFilters: DateFilter[] = [
{ {
id: 'dateAny', id: 'dateAny',
label: attachment.string.FileBrowserDateFilterAny, label: attachment.string.FileBrowserDateFilterAny,
@ -139,11 +150,7 @@ export const dateFileBrowserFilters: {
} }
] ]
export const fileTypeFileBrowserFilters: { export const fileTypeFileBrowserFilters: TypeFilter[] = [
id: string
label: IntlString<{}>
getType: () => any
}[] = [
{ {
id: 'typeAny', id: 'typeAny',
label: attachment.string.FileBrowserTypeFilterAny, 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() const client = getClient()
await client.createDoc(attachment.class.SavedAttachments, preference.space.Preference, { 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 client = getClient()
const current = await client.findOne(attachment.class.SavedAttachments, { attachedTo: attach._id }) const current = await client.findOne(attachment.class.SavedAttachments, { attachedTo: attach._id })
@ -202,6 +209,7 @@ export default async (): Promise<Resources> => ({
component: { component: {
AttachmentsPresenter, AttachmentsPresenter,
AttachmentPresenter, AttachmentPresenter,
AttachmentGalleryPresenter,
Attachments, Attachments,
FileBrowser, FileBrowser,
Photos Photos

View File

@ -28,6 +28,8 @@ export default mergeIds(attachmentId, attachment, {
Photos: '' as IntlString, Photos: '' as IntlString,
FileBrowser: '' as IntlString, FileBrowser: '' as IntlString,
FileBrowserFileCounter: '' as IntlString, FileBrowserFileCounter: '' as IntlString,
FileBrowserListView: '' as IntlString,
FileBrowserGridView: '' as IntlString,
FileBrowserFilterFrom: '' as IntlString, FileBrowserFilterFrom: '' as IntlString,
FileBrowserFilterIn: '' as IntlString, FileBrowserFilterIn: '' as IntlString,
FileBrowserFilterDate: '' as IntlString, FileBrowserFilterDate: '' as IntlString,

View File

@ -229,7 +229,7 @@
.container { .container {
position: relative; position: relative;
display: flex; display: flex;
padding: .5rem 2rem; padding: 0.5rem 2rem;
.avatar { .avatar {
min-width: 2.25rem; min-width: 2.25rem;