Chunter: file browser additional fixes (#1547)

Signed-off-by: Ruslan Izhitsky <ruslan.izhitskiy@xored.com>
This commit is contained in:
Ruslan Izhitsky 2022-04-27 15:47:29 +07:00 committed by GitHub
parent 469ac85e99
commit ca2b5ba0f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 658 additions and 261 deletions

View File

@ -334,7 +334,10 @@ export function createModel (builder: Builder): void {
id: 'fileBrowser',
label: attachment.string.FileBrowser,
icon: attachment.icon.FileBrowser,
component: attachment.component.FileBrowser
component: attachment.component.FileBrowser,
componentProps: {
requestedSpaceClasses: [chunter.class.Channel, chunter.class.DirectMessage]
}
}
],
spaces: [

View File

@ -8,15 +8,18 @@
"Close": "Close",
"NotSelected": "Not selected",
"Deselect": "Deselect",
"Archived": "(archived)",
"AddSocialLinks": "Add social links",
"EditSocialLinks": "Edit social links",
"Change": "Change",
"Remove": "Remove",
"Members": "Members",
"Search": "Search...",
"Spaces": "Spaces",
"Unassigned": "Unassigned",
"CreateMore": "Create more",
"NumberMembers": "{count, plural, =0 {no members} =1 {1 member} other {# members}}",
"NumberSpaces": "{count, plural, =0 {In} =1 {In 1 place} other {In # places}}",
"InThis": "In this {space}",
"NoMatchesInThis": "No matches in this {space}",
"NoMatchesFound": "No matches found",

View File

@ -8,15 +8,18 @@
"Close": "Закрыть",
"NotSelected": "Не выбрано",
"Deselect": "Снять выделение",
"Archived": "(архивирован)",
"AddSocialLinks": "Добавить контактную информацию",
"EditSocialLinks": "Редактировать контактную информацию",
"Change": "Изменить",
"Remove": "Удалить",
"Members": "Участники",
"Search": "Поиск...",
"Spaces": "Пространства",
"Unassigned": "Не назначен",
"CreateMore": "Создать еще",
"NumberMembers": "{count, plural, =0 {нет участников} =1 {1 участник} other {# участника}}",
"NumberSpaces": "{count, plural, =0 {В} =1 {В 1 месте} other {В # местах}}",
"InThis": "В этом {space}",
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
"NoMatchesFound": "Не найдено соответсвий",

View File

@ -14,9 +14,10 @@
-->
<script lang="ts">
import { IconFolder } from '@anticrm/ui'
import { IconFolder, Label } from '@anticrm/ui'
import type { Space } from '@anticrm/core'
import presentation from '..'
export let value: Space
export let subtitle: string | undefined = undefined
@ -27,7 +28,12 @@
<div class="flex-center {size} caption-color flex-no-shrink"><IconFolder size={'small'} /></div>
<div class="flex-col ml-2 min-w-0">
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
<div class="content-accent-color overflow-label">{value.name}</div>
<div class="content-accent-color overflow-label">
{value.name}
{#if value.archived}
<Label label={presentation.string.Archived}/>
{/if}
</div>
</div>
</div>

View File

@ -0,0 +1,73 @@
<!--
// 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 core, { Class, Ref, Space } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
import { Tooltip, showPopup, Button } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import presentation, { SpacesMultiPopup } from '..'
export let selectedItems: Ref<Space>[] = []
export let _classes: Ref<Class<Space>>[] = []
export let label: IntlString
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
export let width: string | undefined = undefined
export let labelDirection: TooltipAlignment | undefined = undefined
const dispatch = createEventDispatcher()
async function addSpace (evt: Event): Promise<void> {
showPopup(
SpacesMultiPopup,
{
_classes,
label,
selectedSpaces: selectedItems
},
evt.target as HTMLElement,
() => { },
(result) => {
if (result !== undefined) {
selectedItems = result
dispatch('update', selectedItems)
}
}
)
}
</script>
<Tooltip {label} fill={width === '100%'} direction={labelDirection}>
<Button
label={selectedItems.length === 0 ? presentation.string.Spaces : undefined}
width={width ?? 'min-content'}
{kind} {size} {justify}
on:click={addSpace}
>
<svelte:fragment slot="content">
{#if selectedItems.length > 0}
<div class="flex-row-center flex-nowrap">
{#await translate(presentation.string.NumberSpaces, { count: selectedItems.length }) then text}
<span class="ml-1-5">{text}</span>
{/await}
</div>
{/if}
</svelte:fragment>
</Button>
</Tooltip>

View File

@ -0,0 +1,114 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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 { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import { createEventDispatcher, onMount } from 'svelte'
import core, { Class, getCurrentAccount, Ref, Space } from '@anticrm/core'
import { Tooltip, CheckBox, Label } from '@anticrm/ui'
import { createQuery } from '../utils'
import presentation from '..'
import SpaceInfo from './SpaceInfo.svelte';
export let _classes: Ref<Class<Space>>[] = []
export let allowDeselect: boolean = false
export let titleDeselect: IntlString | undefined = undefined
export let placeholder: IntlString = presentation.string.Search
export let selected: Ref<Space> | undefined
export let selectedSpaces: Ref<Space>[] = []
let searchQuery: string = ''
let spaces: Space[] = []
let shownSpaces: Space[] = []
let input: HTMLInputElement
const dispatch = createEventDispatcher()
const query = createQuery()
const myAccId = getCurrentAccount()._id
$: query.query<Space>(
core.class.Space,
{
name: { $like: '%' + searchQuery + '%' },
_class: { $in: _classes },
},
result => {
spaces = result
},
{ limit: 200 }
)
$: update(spaces)
const update = (spaces_: Space[]) => {
shownSpaces = spaces_.filter((sp) => {
// don't show archived unless search is specified or this space is selected
// show private only if it includes the current user
return (!sp.archived || searchQuery || selectedSpaces.includes(sp._id)) && (!sp.private || sp.members.includes(myAccId))
})
}
let phTraslate: string = ''
$: if (placeholder) translate(placeholder, {}).then(res => { phTraslate = res })
const isSelected = (space: Space): boolean => {
if (selectedSpaces.filter(s => s === space._id).length > 0) return true
return false
}
const checkSelected = (space: Space): void => {
if (isSelected(space)) {
selectedSpaces = selectedSpaces.filter(s => s !== space._id)
} else {
selectedSpaces.push(space._id)
}
spaces = spaces
dispatch('update', selectedSpaces)
}
onMount(() => { if (input) input.focus() })
</script>
<div class="selectPopup">
<div class="header">
<input bind:this={input} type='text' bind:value={searchQuery} placeholder={phTraslate} on:change/>
</div>
<div class="scroll">
<div class="box">
{#each shownSpaces as space}
<button class="menu-item" on:click={() => {
checkSelected(space)
}}>
<div class="check pointer-events-none">
<CheckBox checked={isSelected(space)} primary />
</div>
<SpaceInfo size={'medium'} value={space} />
{#if allowDeselect && space._id === selected}
<div class="check-right pointer-events-none">
{#if titleDeselect}
<Tooltip label={titleDeselect ?? presentation.string.Deselect}>
<CheckBox checked circle primary />
</Tooltip>
{:else}
<CheckBox checked circle primary />
{/if}
</div>
{/if}
</button>
{/each}
</div>
</div>
</div>

View File

@ -29,6 +29,8 @@ export { default as MessageBox } from './components/MessageBox.svelte'
export { default as MessageViewer } from './components/MessageViewer.svelte'
export { default as PDFViewer } from './components/PDFViewer.svelte'
export { default as SpaceCreateCard } from './components/SpaceCreateCard.svelte'
export { default as SpacesMultiPopup } from './components/SpacesMultiPopup.svelte'
export { default as SpaceMultiBoxList } from './components/SpaceMultiBoxList.svelte'
export { default as SpaceSelect } from './components/SpaceSelect.svelte'
export { default as UserBox } from './components/UserBox.svelte'
export { default as UserBoxList } from './components/UserBoxList.svelte'

View File

@ -37,15 +37,18 @@ export default plugin(presentationId, {
Close: '' as IntlString,
NotSelected: '' as IntlString,
Deselect: '' as IntlString,
Archived: '' as IntlString,
AddSocialLinks: '' as IntlString,
EditSocialLinks: '' as IntlString,
Change: '' as IntlString,
Remove: '' as IntlString,
Members: '' as IntlString,
Search: '' as IntlString,
Spaces: '' as IntlString,
Unassigned: '' as IntlString,
CreateMore: '' as IntlString,
NumberMembers: '' as IntlString,
NumberSpaces: '' as IntlString,
InThis: '' as IntlString,
NoMatchesInThis: '' as IntlString,
NoMatchesFound: '' as IntlString,

View File

@ -21,10 +21,13 @@
"FileBrowserFilterIn": "In",
"FileBrowserFilterDate": "Date",
"FileBrowserFilterFileType": "File type",
"FileBrowserSort": "Sort:",
"FileBrowserSortNewest": "Newest file",
"FileBrowserSortOldest": "Oldest file",
"FileBrowserSortAZ": "A to Z",
"FileBrowserSortZA": "Z to A",
"FileBrowserSortSmallest": "Smallest file",
"FileBrowserSortBiggest": "Biggest file",
"FileBrowserDateFilterAny": "Any time",
"FileBrowserDateFilterToday": "Today",
"FileBrowserDateFilterYesterday": "Yesterday",

View File

@ -21,10 +21,13 @@
"FileBrowserFilterIn": "В",
"FileBrowserFilterDate": "Дата",
"FileBrowserFilterFileType": "Тип файла",
"FileBrowserSort": "Сортировка:",
"FileBrowserSortNewest": "Самый новый файл",
"FileBrowserSortOldest": "Самый старый файл",
"FileBrowserSortAZ": "От А до Я",
"FileBrowserSortZA": "От Я до А",
"FileBrowserSortSmallest": "Самый маленький файл",
"FileBrowserSortBiggest": "Самый большой файл",
"FileBrowserDateFilterAny": "В любое время",
"FileBrowserDateFilterToday": "Сегодня",
"FileBrowserDateFilterYesterday": "Вчера",

View File

@ -16,127 +16,46 @@
import { Attachment } from '@anticrm/attachment'
import contact, { Employee } from '@anticrm/contact'
import { EmployeeAccount } from '@anticrm/contact'
import { Doc, getCurrentAccount, Ref, SortingOrder, SortingQuery, Space } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { getClient, UserBoxList } from '@anticrm/presentation'
import { DropdownLabelsIntl, IconMoreV, Label, Menu as UIMenu, showPopup } from '@anticrm/ui'
import core, { Class, Doc, getCurrentAccount, Ref, Space } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import ui, {
getCurrentLocation,
location,
IconMoreV,
IconSearch,
Label,
showPopup,
navigate,
EditWithIcon,
Spinner
} from '@anticrm/ui'
import { Menu } from '@anticrm/view-resources'
import { AttachmentPresenter } from '..'
import { onDestroy } from 'svelte'
import {
AttachmentPresenter,
FileBrowserSortMode,
dateFileBrowserFilters,
fileTypeFileBrowserFilters,
sortModeToOptionObject
} from '..'
import attachment from '../plugin'
enum SortMode {
NewestFile,
OldestFile,
AscendingAlphabetical,
DescendingAlphabetical
}
const msInDay = 24 * 60 * 60 * 1000
const getBeginningOfToday = () => {
const date = new Date()
date.setUTCHours(0, 0, 0, 0)
return date.getTime()
}
const dateObjects = [
{
id: 'dateAny',
label: attachment.string.FileBrowserDateFilterAny,
getDate: () => {
return undefined
}
},
{
id: 'dateToday',
label: attachment.string.FileBrowserDateFilterToday,
getDate: () => {
return { $gte: getBeginningOfToday() }
}
},
{
id: 'dateYesterday',
label: attachment.string.FileBrowserDateFilterYesterday,
getDate: () => {
return { $gte: getBeginningOfToday() - msInDay, $lt: getBeginningOfToday() }
}
},
{
id: 'date7Days',
label: attachment.string.FileBrowserDateFilter7Days,
getDate: () => {
return { $gte: getBeginningOfToday() - msInDay * 6 }
}
},
{
id: 'date30Days',
label: attachment.string.FileBrowserDateFilter30Days,
getDate: () => {
return { $gte: getBeginningOfToday() - msInDay * 29 }
}
},
{
id: 'date3Months',
label: attachment.string.FileBrowserDateFilter3Months,
getDate: () => {
return { $gte: getBeginningOfToday() - msInDay * 90 }
}
},
{
id: 'date12Months',
label: attachment.string.FileBrowserDateFilter12Months,
getDate: () => {
return { $gte: getBeginningOfToday() - msInDay * 364 }
}
}
]
const fileTypeObjects = [
{
id: 'typeAny',
label: attachment.string.FileBrowserTypeFilterAny,
getType: () => {
return undefined
}
},
{
id: 'typeImage',
label: attachment.string.FileBrowserTypeFilterImages,
getType: () => {
return { $like: '%image/%' }
}
},
{
id: 'typeAudio',
label: attachment.string.FileBrowserTypeFilterAudio,
getType: () => {
return { $like: '%audio/%' }
}
},
{
id: 'typeVideo',
label: attachment.string.FileBrowserTypeFilterVideos,
getType: () => {
return { $like: '%video/%' }
}
},
{
id: 'typePDF',
label: attachment.string.FileBrowserTypeFilterPDFs,
getType: () => {
return 'application/pdf'
}
}
]
import FileBrowserFilters from './FileBrowserFilters.svelte'
import FileBrowserSortMenu from './FileBrowserSortMenu.svelte'
const client = getClient()
export let space: Space | undefined
const loc = getCurrentLocation()
const spaceId: Ref<Space> | undefined = loc.query?.spaceId as Ref<Space> | undefined
export let requestedSpaceClasses: Ref<Class<Space>>[] = []
const currentUser = getCurrentAccount() as EmployeeAccount
let participants: Ref<Employee>[] = [currentUser.employee]
const assignee: Ref<Employee> | null = null
let selectedParticipants: Ref<Employee>[] = [currentUser.employee]
let selectedSpaces: Ref<Space>[] = []
let searchQuery: string = ''
let isLoading = false
let attachments: Attachment[] = []
let selectedFileNumber: number | undefined
let selectedSort: SortMode = SortMode.NewestFile
let selectedSort: FileBrowserSortMode = FileBrowserSortMode.NewestFile
let selectedDateId = 'dateAny'
let selectedFileTypeId = 'typeAny'
@ -147,135 +66,89 @@
})
}
const showSortMenu = async (ev: Event): Promise<void> => {
showPopup(
UIMenu,
{
actions: [
{
label: sortModeToString(SortMode.NewestFile),
action: () => {
selectedSort = SortMode.NewestFile
}
},
{
label: sortModeToString(SortMode.OldestFile),
action: () => {
selectedSort = SortMode.OldestFile
}
},
{
label: sortModeToString(SortMode.AscendingAlphabetical),
action: () => {
selectedSort = SortMode.AscendingAlphabetical
}
},
{
label: sortModeToString(SortMode.DescendingAlphabetical),
action: () => {
selectedSort = SortMode.DescendingAlphabetical
}
}
]
},
ev.target as HTMLElement
)
}
$: fetch(searchQuery, selectedSort, selectedFileTypeId, selectedDateId, selectedParticipants, selectedSpaces)
const sortModeToString = (sortMode: SortMode): IntlString<{}> => {
switch (sortMode) {
case SortMode.NewestFile:
return attachment.string.FileBrowserSortNewest
case SortMode.OldestFile:
return attachment.string.FileBrowserSortOldest
case SortMode.AscendingAlphabetical:
return attachment.string.FileBrowserSortAZ
case SortMode.DescendingAlphabetical:
return attachment.string.FileBrowserSortZA
async function fetch(
searchQuery_: string,
selectedSort_: FileBrowserSortMode,
selectedFileTypeId_: string,
selectedDateId_: string,
selectedParticipants_: Ref<Employee>[],
selectedSpaces_: Ref<Space>[]
) {
isLoading = true
const nameQuery = searchQuery_ ? { name: { $like: '%' + searchQuery_ + '%' } } : {}
const accounts = await client.findAll(contact.class.EmployeeAccount, { employee: { $in: selectedParticipants_ } })
const senderQuery = accounts.length ? { modifiedBy: { $in: accounts.map((a) => a._id) } } : {}
let spaceQuery: { space: any }
if (selectedSpaces_.length) {
spaceQuery = { space: { $in: selectedSpaces_ } }
} else {
// nothing is selected in space filter - show all available attachments (except for the archived channels)
const allSpaces = await client.findAll(core.class.Space, {
archived: false,
_class: { $in: requestedSpaceClasses }
})
const availableSpaces = allSpaces
.filter((sp) => !sp.private || sp.members.includes(currentUser._id))
.map((sp) => sp._id)
spaceQuery = { space: { $in: availableSpaces } }
}
}
const sortModeToOptionObject = (sortMode: SortMode): SortingQuery<Attachment> => {
switch (sortMode) {
case SortMode.NewestFile:
return { modifiedOn: SortingOrder.Descending }
case SortMode.OldestFile:
return { modifiedOn: SortingOrder.Ascending }
case SortMode.AscendingAlphabetical:
return { name: SortingOrder.Ascending }
case SortMode.DescendingAlphabetical:
return { name: SortingOrder.Descending }
}
}
$: fetch(selectedSort, selectedFileTypeId, selectedDateId)
async function fetch (selectedSort_: SortMode, selectedFileTypeId_: string, selectedDateId_: string) {
const spaceQuery = space && { space: space._id }
const fileType = fileTypeObjects.find((o) => o.id === selectedFileTypeId_)?.getType()
const typeQuery = fileType && { type: fileType }
const date = dateObjects.find((o) => o.id === selectedDateId_)?.getDate()
const date = dateFileBrowserFilters.find((o) => o.id === selectedDateId_)?.getDate()
const dateQuery = date && { modifiedOn: date }
const fileType = fileTypeFileBrowserFilters.find((o) => o.id === selectedFileTypeId_)?.getType()
const fileTypeQuery = fileType && { type: fileType }
attachments = await client.findAll(
attachment.class.Attachment,
{ ...spaceQuery, ...typeQuery, ...dateQuery },
{ ...nameQuery, ...senderQuery, ...spaceQuery, ...dateQuery, ...fileTypeQuery },
{
sort: sortModeToOptionObject(selectedSort_)
sort: sortModeToOptionObject(selectedSort_),
limit: 200
}
)
isLoading = false
}
onDestroy(
location.subscribe(async (loc) => {
loc.query = undefined
navigate(loc)
})
)
</script>
<div class="ac-header full divide">
<div class="ac-header__wrap-title">
<span class="ac-header__title"><Label label={attachment.string.FileBrowser} /></span>
</div>
<EditWithIcon icon={IconSearch} bind:value={searchQuery} placeholder={ui.string.SearchDots} />
</div>
<div class="filterBlockContainer">
<div class="simpleFilterButton">
<UserBoxList
_class={contact.class.Employee}
items={participants}
label={attachment.string.FileBrowserFilterFrom}
on:update={(evt) => {
participants = evt.detail
}}
noItems={attachment.string.NoParticipants}
/>
</div>
<!-- TODO: wait for In filter -->
<!-- <div class="simpleFilterButton">
<UserBox _class={contact.class.Employee} label={attachment.string.FileBrowserFilterIn} bind:value={assignee} />
</div> -->
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={dateObjects}
placeholder={attachment.string.FileBrowserFilterDate}
label={attachment.string.FileBrowserFilterDate}
bind:selected={selectedDateId}
/>
</div>
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={fileTypeObjects}
placeholder={attachment.string.FileBrowserFilterFileType}
label={attachment.string.FileBrowserFilterFileType}
bind:selected={selectedFileTypeId}
/>
</div>
</div>
<FileBrowserFilters
{requestedSpaceClasses}
{spaceId}
bind:selectedParticipants
bind:selectedSpaces
bind:selectedDateId
bind:selectedFileTypeId
/>
<div class="group">
<div class="groupHeader">
<div class="eGroupHeaderCount">
<Label label={attachment.string.FileBrowserFileCounter} params={{ results: attachments?.length ?? 0 }} />
</div>
<div class="eGroupHeaderSortMenu" on:click={(event) => showSortMenu(event)}>
{'Sort: '}
<Label label={sortModeToString(selectedSort)} />
</div>
<FileBrowserSortMenu bind:selectedSort />
</div>
{#if attachments?.length}
{#if isLoading}
<div class="ml-4">
<Spinner />
</div>
{:else if attachments?.length}
<div class="flex-col">
{#each attachments as attachment, i}
<div class="flex-between attachmentRow" class:fixed={i === selectedFileNumber}>
@ -299,9 +172,8 @@
<style lang="scss">
.group {
border: 1px solid var(--theme-bg-focused-border);
border-radius: 1rem;
padding: 1rem 0;
overflow: auto;
}
.groupHeader {
@ -313,10 +185,6 @@
font-size: 0.75rem;
color: var(--theme-caption-color);
}
.eGroupHeaderSortMenu {
cursor: pointer;
}
}
.attachmentRow {
@ -360,16 +228,4 @@
}
}
}
.filterBlockContainer {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
margin-bottom: 10px;
}
.simpleFilterButton {
min-width: 4rem;
max-width: 12rem;
margin-left: 0.75rem;
}
</style>

View File

@ -0,0 +1,82 @@
<!--
// 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 contact, { Employee } from '@anticrm/contact'
import { Class, Ref, Space } from '@anticrm/core'
import { SpaceMultiBoxList, UserBoxList } from '@anticrm/presentation'
import { DropdownLabelsIntl } from '@anticrm/ui'
import attachment from '../plugin'
import { dateFileBrowserFilters, fileTypeFileBrowserFilters } from '..'
export let requestedSpaceClasses: Ref<Class<Space>>[]
export let spaceId: Ref<Space> | undefined
export let selectedParticipants: Ref<Employee>[]
export let selectedSpaces: Ref<Space>[]
export let selectedDateId: string
export let selectedFileTypeId: string
</script>
<div class="filterBlockContainer">
<div class="simpleFilterButton">
<UserBoxList
_class={contact.class.Employee}
items={selectedParticipants}
label={attachment.string.FileBrowserFilterFrom}
on:update={(evt) => {
selectedParticipants = evt.detail
}}
noItems={attachment.string.NoParticipants}
/>
</div>
<div class="simpleFilterButton">
<SpaceMultiBoxList
_classes={requestedSpaceClasses}
label={attachment.string.FileBrowserFilterIn}
selectedItems={spaceId ? [spaceId] : []}
on:update={(evt) => {
selectedSpaces = evt.detail
}}
/>
</div>
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={dateFileBrowserFilters}
placeholder={attachment.string.FileBrowserFilterDate}
label={attachment.string.FileBrowserFilterDate}
bind:selected={selectedDateId}
/>
</div>
<div class="simpleFilterButton">
<DropdownLabelsIntl
items={fileTypeFileBrowserFilters}
placeholder={attachment.string.FileBrowserFilterFileType}
label={attachment.string.FileBrowserFilterFileType}
bind:selected={selectedFileTypeId}
/>
</div>
</div>
<style lang="scss">
.filterBlockContainer {
display: flex;
flex-flow: row wrap;
margin-top: 10px;
margin-bottom: 10px;
}
.simpleFilterButton {
max-width: 12rem;
margin-left: 0.75rem;
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts">
import { IntlString } from '@anticrm/platform'
import { Label, Menu, showPopup } from '@anticrm/ui'
import { FileBrowserSortMode } from '..'
import attachment from '../plugin'
export let selectedSort: FileBrowserSortMode
const sortModeToString = (sortMode: FileBrowserSortMode): IntlString<{}> => {
switch (sortMode) {
case FileBrowserSortMode.NewestFile:
return attachment.string.FileBrowserSortNewest
case FileBrowserSortMode.OldestFile:
return attachment.string.FileBrowserSortOldest
case FileBrowserSortMode.AscendingAlphabetical:
return attachment.string.FileBrowserSortAZ
case FileBrowserSortMode.DescendingAlphabetical:
return attachment.string.FileBrowserSortZA
case FileBrowserSortMode.SmallestSize:
return attachment.string.FileBrowserSortSmallest
case FileBrowserSortMode.BiggestSize:
return attachment.string.FileBrowserSortBiggest
}
}
const showSortMenu = async (ev: Event): Promise<void> => {
showPopup(
Menu,
{
actions: [
{
label: sortModeToString(FileBrowserSortMode.NewestFile),
action: () => {
selectedSort = FileBrowserSortMode.NewestFile
}
},
{
label: sortModeToString(FileBrowserSortMode.OldestFile),
action: () => {
selectedSort = FileBrowserSortMode.OldestFile
}
},
{
label: sortModeToString(FileBrowserSortMode.AscendingAlphabetical),
action: () => {
selectedSort = FileBrowserSortMode.AscendingAlphabetical
}
},
{
label: sortModeToString(FileBrowserSortMode.DescendingAlphabetical),
action: () => {
selectedSort = FileBrowserSortMode.DescendingAlphabetical
}
},
{
label: sortModeToString(FileBrowserSortMode.SmallestSize),
action: () => {
selectedSort = FileBrowserSortMode.SmallestSize
}
},
{
label: sortModeToString(FileBrowserSortMode.BiggestSize),
action: () => {
selectedSort = FileBrowserSortMode.BiggestSize
}
}
]
},
ev.target as HTMLElement
)
}
</script>
<div class="sortMenu" on:click={(event) => showSortMenu(event)}>
<Label label={attachment.string.FileBrowserSort} />
<Label label={sortModeToString(selectedSort)} />
</div>
<style lang="scss">
.sortMenu {
cursor: pointer;
}
</style>

View File

@ -19,11 +19,25 @@
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g {fill}>
<path d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z"/>
<path d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z"/>
<path d="M40.489,27.248c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.119 C43.746,29.177,42.338,27.573,40.489,27.248z"/>
<path d="M68.792,27.211c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.082 C72.049,29.14,70.641,27.535,68.792,27.211z"/>
<path d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/>
<path d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/>
<path
d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z"
/>
<path
d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082c0-2.176,1.764-3.94,3.94-3.94h8.915 c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z"
/>
<path
d="M40.489,27.248c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.119 C43.746,29.177,42.338,27.573,40.489,27.248z"
/>
<path
d="M68.792,27.211c0.769,0.719,1.257,1.735,1.257,2.871v37.799c0,2.176-1.764,3.94-3.94,3.94h-8.915 c-0.234,0-0.46-0.03-0.683-0.069c0.704,0.658,1.643,1.069,2.683,1.069h8.915c2.176,0,3.94-1.764,3.94-3.94V31.082 C72.049,29.14,70.641,27.535,68.792,27.211z"
/>
<path
d="M39.806,72.858h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.119 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C43.746,71.094,41.982,72.858,39.806,72.858z"
style="fill:none;stroke:#000000;stroke-miterlimit:10;"
/>
<path
d="M68.109,72.821h-8.915c-2.176,0-3.94-1.764-3.94-3.94V31.082 c0-2.176,1.764-3.94,3.94-3.94h8.915c2.176,0,3.94,1.764,3.94,3.94v37.799C72.049,71.057,70.285,72.821,68.109,72.821z"
style="fill:none;stroke:#000000;stroke-miterlimit:10;"
/>
</g>
</svg>

View File

@ -19,8 +19,15 @@
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g {fill}>
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7 C27.852,25.902,29.798,24.778,31.356,25.677z"/>
<path d="M69.981,47.977l-38.625-22.3c-0.233-0.134-0.474-0.21-0.716-0.259l37.341,21.559c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-0.349,0.201-0.716,0.288-1.078,0.301c0.656,0.938,1.961,1.343,3.078,0.699l38.625-22.3 C71.538,51.124,71.538,48.876,69.981,47.977z"/>
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7C27.852,25.902,29.798,24.778,31.356,25.677z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/>
<path
d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7 C27.852,25.902,29.798,24.778,31.356,25.677z"
/>
<path
d="M69.981,47.977l-38.625-22.3c-0.233-0.134-0.474-0.21-0.716-0.259l37.341,21.559c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-0.349,0.201-0.716,0.288-1.078,0.301c0.656,0.938,1.961,1.343,3.078,0.699l38.625-22.3 C71.538,51.124,71.538,48.876,69.981,47.977z"
/>
<path
d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7C27.852,25.902,29.798,24.778,31.356,25.677z"
style="fill:none;stroke:#000000;stroke-miterlimit:10;"
/>
</g>
</svg>

View File

@ -24,9 +24,10 @@ import TxAttachmentCreate from './components/activity/TxAttachmentCreate.svelte'
import Attachments from './components/Attachments.svelte'
import FileBrowser from './components/FileBrowser.svelte'
import Photos from './components/Photos.svelte'
import { Resources } from '@anticrm/platform'
import { uploadFile, deleteFile } from './utils'
import attachment, { Attachment } from '@anticrm/attachment'
import { SortingOrder, SortingQuery } from '@anticrm/core'
import { IntlString, Resources } from '@anticrm/platform'
import preference from '@anticrm/preference'
import { getClient } from '@anticrm/presentation'
@ -41,6 +42,143 @@ export {
AttachmentDocList
}
export enum FileBrowserSortMode {
NewestFile,
OldestFile,
AscendingAlphabetical,
DescendingAlphabetical,
SmallestSize,
BiggestSize
}
export const sortModeToOptionObject = (sortMode: FileBrowserSortMode): SortingQuery<Attachment> => {
switch (sortMode) {
case FileBrowserSortMode.NewestFile:
return { modifiedOn: SortingOrder.Descending }
case FileBrowserSortMode.OldestFile:
return { modifiedOn: SortingOrder.Ascending }
case FileBrowserSortMode.AscendingAlphabetical:
return { name: SortingOrder.Ascending }
case FileBrowserSortMode.DescendingAlphabetical:
return { name: SortingOrder.Descending }
case FileBrowserSortMode.SmallestSize:
return { size: SortingOrder.Ascending }
case FileBrowserSortMode.BiggestSize:
return { size: SortingOrder.Descending }
}
}
const msInDay = 24 * 60 * 60 * 1000
const getBeginningOfDate = (customDate?: Date) => {
if (!customDate) {
customDate = new Date()
}
customDate.setUTCHours(0, 0, 0, 0)
return customDate.getTime()
}
export const dateFileBrowserFilters: {
id: string
label: IntlString<{}>
getDate: () => any
}[] = [
{
id: 'dateAny',
label: attachment.string.FileBrowserDateFilterAny,
getDate: () => {
return undefined
}
},
{
id: 'dateToday',
label: attachment.string.FileBrowserDateFilterToday,
getDate: () => {
return { $gte: getBeginningOfDate() }
}
},
{
id: 'dateYesterday',
label: attachment.string.FileBrowserDateFilterYesterday,
getDate: () => {
return { $gte: getBeginningOfDate() - msInDay, $lt: getBeginningOfDate() }
}
},
{
id: 'date7Days',
label: attachment.string.FileBrowserDateFilter7Days,
getDate: () => {
return { $gte: getBeginningOfDate() - msInDay * 6 }
}
},
{
id: 'date30Days',
label: attachment.string.FileBrowserDateFilter30Days,
getDate: () => {
return { $gte: getBeginningOfDate() - msInDay * 29 }
}
},
{
id: 'date3Months',
label: attachment.string.FileBrowserDateFilter3Months,
getDate: () => {
const now = new Date()
now.setMonth(now.getMonth() - 3)
return { $gte: getBeginningOfDate(now) }
}
},
{
id: 'date12Months',
label: attachment.string.FileBrowserDateFilter12Months,
getDate: () => {
const now = new Date()
now.setMonth(now.getMonth() - 12)
return { $gte: getBeginningOfDate(now) }
}
}
]
export const fileTypeFileBrowserFilters: {
id: string
label: IntlString<{}>
getType: () => any
}[] = [
{
id: 'typeAny',
label: attachment.string.FileBrowserTypeFilterAny,
getType: () => {
return undefined
}
},
{
id: 'typeImage',
label: attachment.string.FileBrowserTypeFilterImages,
getType: () => {
return { $like: '%image/%' }
}
},
{
id: 'typeAudio',
label: attachment.string.FileBrowserTypeFilterAudio,
getType: () => {
return { $like: '%audio/%' }
}
},
{
id: 'typeVideo',
label: attachment.string.FileBrowserTypeFilterVideos,
getType: () => {
return { $like: '%video/%' }
}
},
{
id: 'typePDF',
label: attachment.string.FileBrowserTypeFilterPDFs,
getType: () => {
return 'application/pdf'
}
}
]
export async function AddAttachmentToSaved(attach: Attachment): Promise<void> {
const client = getClient()

View File

@ -32,22 +32,13 @@ export default mergeIds(attachmentId, attachment, {
FileBrowserFilterIn: '' as IntlString,
FileBrowserFilterDate: '' as IntlString,
FileBrowserFilterFileType: '' as IntlString,
FileBrowserSort: '' as IntlString,
FileBrowserSortNewest: '' as IntlString,
FileBrowserSortOldest: '' as IntlString,
FileBrowserSortAZ: '' as IntlString,
FileBrowserSortZA: '' as IntlString,
FileBrowserDateFilterAny: '' as IntlString,
FileBrowserDateFilterToday: '' as IntlString,
FileBrowserDateFilterYesterday: '' as IntlString,
FileBrowserDateFilter7Days: '' as IntlString,
FileBrowserDateFilter30Days: '' as IntlString,
FileBrowserDateFilter3Months: '' as IntlString,
FileBrowserDateFilter12Months: '' as IntlString,
FileBrowserTypeFilterAny: '' as IntlString,
FileBrowserTypeFilterImages: '' as IntlString,
FileBrowserTypeFilterAudio: '' as IntlString,
FileBrowserTypeFilterVideos: '' as IntlString,
FileBrowserTypeFilterPDFs: '' as IntlString,
FileBrowserSortSmallest: '' as IntlString,
FileBrowserSortBiggest: '' as IntlString,
AddAttachmentToSaved: '' as IntlString,
RemoveAttachmentFromSaved: '' as IntlString
},

View File

@ -71,6 +71,18 @@ export default plugin(attachmentId, {
Files: '' as IntlString,
NoFiles: '' as IntlString,
NoParticipants: '' as IntlString,
ShowMoreAttachments: '' as IntlString
ShowMoreAttachments: '' as IntlString,
FileBrowserDateFilterAny: '' as IntlString,
FileBrowserDateFilterToday: '' as IntlString,
FileBrowserDateFilterYesterday: '' as IntlString,
FileBrowserDateFilter7Days: '' as IntlString,
FileBrowserDateFilter30Days: '' as IntlString,
FileBrowserDateFilter3Months: '' as IntlString,
FileBrowserDateFilter12Months: '' as IntlString,
FileBrowserTypeFilterAny: '' as IntlString,
FileBrowserTypeFilterImages: '' as IntlString,
FileBrowserTypeFilterAudio: '' as IntlString,
FileBrowserTypeFilterVideos: '' as IntlString,
FileBrowserTypeFilterPDFs: '' as IntlString
}
})

View File

@ -77,6 +77,7 @@
on:click={() => {
const loc = getCurrentLocation()
loc.path[2] = 'fileBrowser'
loc.query = channel ? { spaceId: channel._id } : {}
navigate(loc)
}}
>