Chunter: file browser (#1407) (#1488)

Signed-off-by: Ruslan Izhitsky <ruslan.izhitskiy@xored.com>
This commit is contained in:
Ruslan Izhitsky 2022-04-23 09:40:38 +07:00 committed by GitHub
parent 1bfce26be4
commit 53821621c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 700 additions and 83 deletions

View File

@ -23,7 +23,8 @@ import type { TxViewlet } from '@anticrm/activity'
export default mergeIds(attachmentId, attachment, {
component: {
AttachmentPresenter: '' as AnyComponent
AttachmentPresenter: '' as AnyComponent,
FileBrowser: '' as AnyComponent
},
string: {
AddAttachment: '' as IntlString,

View File

@ -329,6 +329,12 @@ export function createModel (builder: Builder): void {
label: chunter.string.SavedMessages,
icon: chunter.icon.Bookmark,
component: chunter.component.SavedMessages
},
{
id: 'fileBrowser',
label: attachment.string.FileBrowser,
icon: attachment.icon.FileBrowser,
component: attachment.component.FileBrowser
}
],
spaces: [

View File

@ -0,0 +1,75 @@
<!--
// 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 { IntlString, Asset } from '@anticrm/platform'
import DropdownLabelsPopupIntl from './DropdownLabelsPopupIntl.svelte';
import type { AnySvelteComponent, TooltipAlignment, ButtonKind, ButtonSize, DropdownIntlItem } from '../types'
import { showPopup, Tooltip, Button, Label } from '..'
import { createEventDispatcher } from 'svelte'
import ui from '../plugin'
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let label: IntlString
export let placeholder: IntlString | undefined = ui.string.SearchDots
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
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
let container: HTMLElement
let opened: boolean = false
let isDisabled = false
$: isDisabled = items.length === 0
let selectedItem = items.find((x) => x.id === selected)
$: selectedItem = items.find((x) => x.id === selected)
$: if (selected === undefined && items[0] !== undefined) {
selected = items[0].id
}
const dispatch = createEventDispatcher()
const none = ui.string.None
</script>
<div bind:this={container} class="min-w-0">
<Tooltip label={label} fill={width === '100%'} direction={labelDirection}>
<Button
{icon}
width={width ?? 'min-content'}
{size} {kind} {justify}
on:click={() => {
if (!opened) {
opened = true
showPopup(DropdownLabelsPopupIntl, { placeholder, items, selected }, container, (result) => {
if (result) {
selected = result
dispatch('selected', result)
}
opened = false
})
}
}}
>
<span slot="content" style="overflow: hidden">
{#if selectedItem} <Label label={selectedItem.label} /> {:else} <Label label={label ?? ui.string.NotSelected} />{/if}
</span>
</Button>
</Tooltip>
</div>

View File

@ -0,0 +1,71 @@
<!--
// 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 { IntlString } from '@anticrm/platform'
import { translate } from '@anticrm/platform'
import { createEventDispatcher, onMount } from 'svelte'
import CheckBox from './CheckBox.svelte'
import type { DropdownIntlItem } from '../types'
import plugin from '../plugin'
import { Label } from '..';
export let placeholder: IntlString = plugin.string.SearchDots
export let items: DropdownIntlItem[]
export let selected: DropdownIntlItem['id'] | undefined = undefined
let search: string = ''
let phTraslate: string = ''
$: translate(placeholder, {}).then(res => { phTraslate = res })
const dispatch = createEventDispatcher()
let btns: HTMLButtonElement[] = []
let searchInput: HTMLInputElement
const keyDown = (ev: KeyboardEvent, n: number): void => {
if (ev.key === 'ArrowDown') {
if (n === btns.length - 1) btns[0].focus()
else btns[n + 1].focus()
} else if (ev.key === 'ArrowUp') {
if (n === 0) btns[btns.length - 1].focus()
else btns[n - 1].focus()
} else searchInput.focus()
}
onMount(() => { if (searchInput) searchInput.focus() })
</script>
<div class="selectPopup">
<div class="header">
<input bind:this={searchInput} type='text' bind:value={search} placeholder={phTraslate} on:input={(ev) => { }} on:change/>
</div>
<div class="scroll">
<div class="box">
{#each items.filter((x) => x.label.toLowerCase().includes(search.toLowerCase())) as item, i}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item flex-between"
on:mouseover={(ev) => ev.currentTarget.focus()}
on:keydown={(ev) => keyDown(ev, i)}
on:click={() => { dispatch('close', item.id) }}
>
<div class="flex-grow caption-color lines-limit-2"> <Label label={item.label} /> </div>
{#if item.id === selected}
<div class="check-right"><CheckBox checked primary /></div>
{/if}
</button>
{/each}
</div>
</div>
</div>

View File

@ -71,6 +71,7 @@ export { default as TimeSince } from './components/TimeSince.svelte'
export { default as Dropdown } from './components/Dropdown.svelte'
export { default as DropdownPopup } from './components/DropdownPopup.svelte'
export { default as DropdownLabels } from './components/DropdownLabels.svelte'
export { default as DropdownLabelsIntl } from './components/DropdownLabelsIntl.svelte'
export { default as ShowMore } from './components/ShowMore.svelte'
export { default as Menu } from './components/Menu.svelte'
export { default as TimeShiftPicker } from './components/TimeShiftPicker.svelte'

View File

@ -97,3 +97,8 @@ export interface DropdownTextItem {
id: string
label: string
}
export interface DropdownIntlItem {
id: string
label: IntlString
}

View File

@ -9,4 +9,7 @@
<symbol id="lock" viewBox="0 0 16 16">
<path d="M12,7.1h-0.7V5.4c0-1.8-1.5-3.3-3.3-3.3c-1.8,0-3.3,1.5-3.3,3.3v1.7H4c-0.8,0-1.5,0.7-1.5,1.5v4.1c0,0.8,0.7,1.5,1.5,1.5h8 c0.8,0,1.5-0.7,1.5-1.5V8.6C13.5,7.8,12.8,7.1,12,7.1z M5.7,5.4c0-1.2,1-2.3,2.3-2.3s2.3,1,2.3,2.3v1.7H5.7V5.4z M12.5,12.7 c0,0.3-0.2,0.5-0.5,0.5H4c-0.3,0-0.5-0.2-0.5-0.5V8.6c0-0.3,0.2-0.5,0.5-0.5h8c0.3,0,0.5,0.2,0.5,0.5V12.7z"/>
</symbol>
<symbol id="fileBrowser" viewBox="0 0 16 16">
<path d="M12.6541 10.7952L14.7544 11.6213C14.8576 11.6618 14.9394 11.7434 14.9801 11.8466C15.0511 12.0264 14.9828 12.2268 14.827 12.3284L14.755 12.3656L8.35645 14.8924C8.15935 14.9703 7.94372 14.9831 7.74052 14.9309L7.62035 14.8918L1.25259 12.3653C1.1499 12.3246 1.06864 12.2432 1.02806 12.1404C0.957068 11.9607 1.02536 11.7603 1.1812 11.6587L1.25319 11.6215L3.34307 10.7962L7.06917 12.2751C7.65895 12.5091 8.31525 12.5097 8.9054 12.2766L12.6541 10.7952ZM12.6541 6.77688L14.7544 7.60289C14.8576 7.64346 14.9394 7.72508 14.9801 7.82824C15.0511 8.00803 14.9828 8.20839 14.827 8.31004L14.755 8.3472L10.6001 9.98825L9.619 10.375L8.35645 10.8741L8.317 10.886L8.23566 10.9132C8.20301 10.9215 8.17004 10.9282 8.13688 10.9331C8.12585 10.9346 8.11547 10.936 8.10507 10.9372C8.02541 10.9468 7.94422 10.9464 7.86397 10.9363L7.74052 10.9126L7.62035 10.8735L6.391 10.385L5.38907 9.98825L1.25259 8.34697C1.1499 8.30623 1.06864 8.22483 1.02806 8.12208C0.957068 7.94229 1.02536 7.74192 1.1812 7.64029L1.25319 7.60312L3.34307 6.77788L7.06917 8.25677C7.65895 8.49078 8.31525 8.4913 8.9054 8.25824L12.6541 6.77688ZM7.62186 1.06989C7.85734 0.976906 8.11932 0.976697 8.35494 1.06931L14.7544 3.58452C14.8576 3.62509 14.9394 3.70671 14.9801 3.80987C15.0612 4.01534 14.9605 4.24769 14.755 4.32884L10.6001 5.96988L8.35565 6.856L8.27468 6.88396C8.25405 6.8901 8.23326 6.89557 8.21236 6.90036C8.09824 6.92674 7.98013 6.93258 7.86397 6.91788L7.74052 6.89419L7.62035 6.8551L1.25259 4.3286C1.1499 4.28786 1.06864 4.20646 1.02806 4.10371C0.946925 3.89823 1.04772 3.66589 1.25319 3.58475L7.62186 1.06989Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -2,6 +2,7 @@
"string": {
"UploadDropFilesHere": "Upload or drop files here",
"NoAttachments": "There are no attachments for this",
"NoParticipants": "No participants added",
"AddAttachment": "uploaded an attachment",
"Attachments": "Attachments",
"Photos": "Photos",
@ -13,6 +14,28 @@
"Size": "Size",
"Photo": "Photo",
"Date": "Date",
"Name": "Name"
"Name": "Name",
"FileBrowser": "File browser",
"FileBrowserFileCounter": "{results, plural, =1 {# result} other {# results}}",
"FileBrowserFilterFrom": "From",
"FileBrowserFilterIn": "In",
"FileBrowserFilterDate": "Date",
"FileBrowserFilterFileType": "File type",
"FileBrowserSortNewest": "Newest file",
"FileBrowserSortOldest": "Oldest file",
"FileBrowserSortAZ": "A to Z",
"FileBrowserSortZA": "Z to A",
"FileBrowserDateFilterAny": "Any time",
"FileBrowserDateFilterToday": "Today",
"FileBrowserDateFilterYesterday": "Yesterday",
"FileBrowserDateFilter7Days": "Last 7 days",
"FileBrowserDateFilter30Days": "Last 30 days",
"FileBrowserDateFilter3Months": "Last 3 months",
"FileBrowserDateFilter12Months": "Last 12 months",
"FileBrowserTypeFilterAny": "Any file type",
"FileBrowserTypeFilterImages": "Images",
"FileBrowserTypeFilterAudio": "Audio",
"FileBrowserTypeFilterVideos": "Videos",
"FileBrowserTypeFilterPDFs": "PDFs"
}
}

View File

@ -2,6 +2,7 @@
"string": {
"UploadDropFilesHere": "Загрузите или перетащите файлы сюда",
"NoAttachments": "Нет вложений",
"NoParticipants": "Участники не добавлены",
"AddAttachment": "загрузил вложение",
"Attachments": "Вложения",
"Photos": "Фотографии",
@ -13,6 +14,28 @@
"Size": "Размер",
"Photo": "Фотография",
"Date": "Дата",
"Name": "Название"
"Name": "Название",
"FileBrowser": "Браузер файлов",
"FileBrowserFileCounter": "{results, plural, =1 {# результат} =2 {# результата} =3 {# результата} =4 {# результата} other {# результатов}}",
"FileBrowserFilterFrom": "От",
"FileBrowserFilterIn": "В",
"FileBrowserFilterDate": "Дата",
"FileBrowserFilterFileType": "Тип файла",
"FileBrowserSortNewest": "Самый новый файл",
"FileBrowserSortOldest": "Самый старый файл",
"FileBrowserSortAZ": "От А до Я",
"FileBrowserSortZA": "От Я до А",
"FileBrowserDateFilterAny": "В любое время",
"FileBrowserDateFilterToday": "Сегодня",
"FileBrowserDateFilterYesterday": "Вчера",
"FileBrowserDateFilter7Days": "Последние 7 дней",
"FileBrowserDateFilter30Days": "Последние 30 дней",
"FileBrowserDateFilter3Months": "Последние 3 месяца",
"FileBrowserDateFilter12Months": "Последние 12 месяцев",
"FileBrowserTypeFilterAny": "Любой тип файла",
"FileBrowserTypeFilterImages": "Изображения",
"FileBrowserTypeFilterAudio": "Звук",
"FileBrowserTypeFilterVideos": "Видео",
"FileBrowserTypeFilterPDFs": "PDF-файлы"
}
}

View File

@ -18,7 +18,8 @@ import attachment, { attachmentId } from '@anticrm/attachment'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(attachment.icon, {
Attachment: `${icons}#chunter`
Attachment: `${icons}#chunter`,
FileBrowser: `${icons}#fileBrowser`
})
addStringsLoader(attachmentId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -36,6 +36,7 @@
"@anticrm/ui": "~0.6.0",
"@anticrm/presentation": "~0.6.2",
"@anticrm/attachment": "~0.6.1",
"@anticrm/contact": "~0.6.5",
"@anticrm/core": "~0.6.16",
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",

View File

@ -29,7 +29,7 @@
const client = getClient()
const dispatch = createEventDispatcher()
async function fileSelected() {
async function fileSelected () {
const list = inputFile.files
if (list === null || list.length === 0) return
@ -43,7 +43,7 @@
dispatch('attached')
}
function openFile() {
function openFile () {
inputFile.click()
}
</script>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Attachment } from '@anticrm/attachment'
import type { Doc } from '@anticrm/core'
@ -29,9 +28,13 @@
function updateQuery (value: Doc & { attachments?: number }): void {
if (value && value.attachments && value.attachments > 0) {
query.query(attachment.class.Attachment, {
attachedTo: value._id
}, (res) => attachments = res)
query.query(
attachment.class.Attachment,
{
attachedTo: value._id
},
(res) => (attachments = res)
)
} else {
attachments = []
}

View File

@ -28,7 +28,7 @@
const client = getClient()
async function fileDrop(e: DragEvent) {
async function fileDrop (e: DragEvent) {
dragover = false
if (canDrop && !canDrop(e)) {

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { Attachment } from '@anticrm/attachment'
import AttachmentPreview from './AttachmentPreview.svelte'
@ -21,9 +20,9 @@
</script>
{#if attachments.length}
<div class='container'>
<div class="container">
{#each attachments as attachment}
<div class='item'>
<div class="item">
<AttachmentPreview value={attachment} />
</div>
{/each}

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Ref, Doc } from '@anticrm/core'
import { Table } from '@anticrm/view-resources'
@ -22,13 +21,12 @@
export let objectId: Ref<Doc>
export let attachments: number
</script>
<Table
<Table
_class={attachment.class.Attachment}
config={['', 'lastModified']}
options={ {} }
query={ { attachedTo: objectId } }
options={{}}
query={{ attachedTo: objectId }}
loadingProps={{ length: attachments }}
/>

View File

@ -33,7 +33,6 @@
let inputFile: HTMLInputElement
let loading = 0
let dragover = false
</script>
<div class="attachments-container">
@ -66,7 +65,8 @@
config={['', 'lastModified']}
options={{}}
query={{ attachedTo: objectId }}
loadingProps={{ length: attachments ?? 0 }} />
loadingProps={{ length: attachments ?? 0 }}
/>
{/if}
</div>
@ -93,5 +93,4 @@
border-style: solid;
}
}
</style>

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Doc } from '@anticrm/core'
import { Tooltip, IconAttachment } from '@anticrm/ui'
@ -26,9 +25,13 @@
</script>
{#if value && value.attachments && value.attachments > 0}
<Tooltip label={attachment.string.Attachments} component={AttachmentPopup} props={{ objectId: value._id, attachments: value.attachments }}>
<Tooltip
label={attachment.string.Attachments}
component={AttachmentPopup}
props={{ objectId: value._id, attachments: value.attachments }}
>
<div class="sm-tool-icon ml-1 mr-1">
<span class="icon"><IconAttachment {size}/></span>
<span class="icon"><IconAttachment {size} /></span>
{#if showCounter}
&nbsp;{value.attachments}
{/if}

View File

@ -12,7 +12,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 { getFileUrl } from '@anticrm/presentation'
@ -23,8 +22,8 @@
export let value: Attachment
let time = 0
let duration = Number.POSITIVE_INFINITY
let paused = true
let duration = Number.POSITIVE_INFINITY
let paused = true
function buttonClick () {
paused = !paused
@ -33,16 +32,16 @@
$: icon = !paused ? Pause : Play
</script>
<div class='container flex-between'>
<div class="container flex-between">
<div>
<CircleButton size='x-large' on:click={buttonClick} {icon} />
<CircleButton size="x-large" on:click={buttonClick} {icon} />
</div>
<div class='w-full ml-4'>
<div class="w-full ml-4">
<Progress bind:value={time} max={Number.isFinite(duration) ? duration : 100} editable />
</div>
</div>
<audio bind:duration bind:currentTime={time} bind:paused>
<source src={getFileUrl(value.file)} type={value.type}>
<source src={getFileUrl(value.file)} type={value.type} />
</audio>
<style lang="scss">
@ -53,4 +52,4 @@
width: 20rem;
padding: 0.5rem;
}
</style>
</style>

View File

@ -0,0 +1,375 @@
<!--
// 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 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 { Menu } from '@anticrm/view-resources'
import { AttachmentPresenter } 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'
}
}
]
const client = getClient()
export let space: Space | undefined
const currentUser = getCurrentAccount() as EmployeeAccount
let participants: Ref<Employee>[] = [currentUser.employee]
const assignee: Ref<Employee> | null = null
let attachments: Attachment[] = []
let selectedFileNumber: number | undefined
let selectedSort: SortMode = SortMode.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
})
}
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
)
}
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
}
}
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 dateQuery = date && { modifiedOn: date }
attachments = await client.findAll(
attachment.class.Attachment,
{ ...spaceQuery, ...typeQuery, ...dateQuery },
{
sort: sortModeToOptionObject(selectedSort_)
}
)
}
</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>
</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>
<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>
</div>
{#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}>
<div id="context-menu" class="eAttachmentRowMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
<IconMoreV size={'small'} />
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class="flex-between attachmentRow">
<Label label={attachment.string.NoFiles} />
</div>
{/if}
</div>
<style lang="scss">
.group {
border: 1px solid var(--theme-bg-focused-border);
border-radius: 1rem;
padding: 1rem 0;
}
.groupHeader {
margin: 0 1.5rem 0.75rem 1.5rem;
display: flex;
justify-content: space-between;
.eGroupHeaderCount {
font-size: 0.75rem;
color: var(--theme-caption-color);
}
.eGroupHeaderSortMenu {
cursor: pointer;
}
}
.attachmentRow {
display: flex;
align-items: center;
padding-right: 1rem;
margin: 0 1.5rem;
padding: 0.25rem 0;
.eAttachmentRowActions {
visibility: hidden;
border: 1px solid var(--theme-bg-focused-border);
padding: 0.2rem;
border-radius: 0.375rem;
}
.eAttachmentRowMenu {
visibility: hidden;
opacity: 0.6;
cursor: pointer;
&:hover {
opacity: 1;
}
}
&:hover {
.eAttachmentRowActions {
visibility: visible;
}
.eAttachmentRowMenu {
visibility: visible;
}
}
&.fixed {
.eAttachmentRowActions {
visibility: visible;
}
.eAttachmentRowMenu {
visibility: visible;
}
}
}
.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

@ -12,14 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Attachment } from "@anticrm/attachment"
import type { TxCreateDoc } from "@anticrm/core"
import type { Attachment } from '@anticrm/attachment'
import type { TxCreateDoc } from '@anticrm/core'
import { TxProcessor } from '@anticrm/core'
import AttachmentPresenter from "../AttachmentPresenter.svelte"
import AttachmentPresenter from '../AttachmentPresenter.svelte'
export let tx: TxCreateDoc<Attachment>
</script>
<AttachmentPresenter value={TxProcessor.createDoc2Doc(tx)}/>
<AttachmentPresenter value={TxProcessor.createDoc2Doc(tx)} />

View File

@ -22,17 +22,28 @@ import AttachmentList from './components/AttachmentList.svelte'
import AttachmentRefInput from './components/AttachmentRefInput.svelte'
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'
export { AddAttachment, AttachmentDroppable, Attachments, AttachmentsPresenter, AttachmentPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
export {
AddAttachment,
AttachmentDroppable,
Attachments,
AttachmentsPresenter,
AttachmentPresenter,
AttachmentRefInput,
AttachmentList,
AttachmentDocList
}
export default async (): Promise<Resources> => ({
component: {
AttachmentsPresenter,
AttachmentPresenter,
Attachments,
FileBrowser,
Photos
},
activity: {

View File

@ -24,6 +24,28 @@ export default mergeIds(attachmentId, attachment, {
NoAttachments: '' as IntlString,
UploadDropFilesHere: '' as IntlString,
Attachments: '' as IntlString,
Photos: '' as IntlString
Photos: '' as IntlString,
FileBrowser: '' as IntlString,
FileBrowserFileCounter: '' as IntlString,
FileBrowserFilterFrom: '' as IntlString,
FileBrowserFilterIn: '' as IntlString,
FileBrowserFilterDate: '' as IntlString,
FileBrowserFilterFileType: '' 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
}
})

View File

@ -20,7 +20,7 @@ import { getMetadata, setPlatformStatus, unknownError } from '@anticrm/platform'
import attachment from './plugin'
export async function uploadFile(file: File, opts?: { space: Ref<Space>; attachedTo: Ref<Doc> }): Promise<string> {
export async function uploadFile (file: File, opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }): Promise<string> {
const uploadUrl = getMetadata(login.metadata.UploadUrl)
if (uploadUrl === undefined) {
@ -58,7 +58,7 @@ export async function uploadFile(file: File, opts?: { space: Ref<Space>; attache
return await resp.text()
}
export async function deleteFile(id: string): Promise<void> {
export async function deleteFile (id: string): Promise<void> {
const uploadUrl = getMetadata(login.metadata.UploadUrl)
const url = `${uploadUrl as string}?file=${id}`
@ -74,10 +74,10 @@ export async function deleteFile(id: string): Promise<void> {
}
}
export async function createAttachments(
export async function createAttachments (
client: Client,
list: FileList,
attachTo: { objectClass: Ref<Class<Doc>>; space: Ref<Space>; objectId: Ref<Doc> }
attachTo: { objectClass: Ref<Class<Doc>>, space: Ref<Space>, objectId: Ref<Doc> }
) {
const { objectClass, objectId, space } = attachTo
try {
@ -100,7 +100,7 @@ export async function createAttachments(
}
}
export function getType(type: string): 'image' | 'video' | 'audio' | 'pdf' | 'other' {
export function getType (type: string): 'image' | 'video' | 'audio' | 'pdf' | 'other' {
if (type.startsWith('image/')) {
return 'image'
}

View File

@ -47,7 +47,8 @@ export default plugin(attachmentId, {
AttachmentsPresenter: '' as AnyComponent
},
icon: {
Attachment: '' as Asset
Attachment: '' as Asset,
FileBrowser: '' as Asset
},
class: {
Attachment: '' as Ref<Class<Attachment>>,
@ -60,6 +61,7 @@ export default plugin(attachmentId, {
string: {
Files: '' as IntlString,
NoFiles: '' as IntlString,
NoParticipants: '' as IntlString,
ShowMoreAttachments: '' as IntlString
}
})

View File

@ -20,14 +20,14 @@
import { Doc, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { Menu } from '@anticrm/view-resources'
import { showPopup, IconMoreV, Label } from '@anticrm/ui'
import { getCurrentLocation, showPopup, IconMoreV, Label, navigate } from '@anticrm/ui'
export let channel: ChunterSpace | undefined
const query = createQuery()
let visibleAttachments: Attachment[] | undefined
let totalAttachments = 0
let attachmentsLimit: number | undefined = 5
const ATTACHEMNTS_LIMIT = 5
let selectedRowNumber: number | undefined
const sort = { modifiedOn: SortingOrder.Descending }
@ -48,14 +48,10 @@
visibleAttachments = res
totalAttachments = res.total
},
attachmentsLimit
? {
limit: attachmentsLimit,
sort: sort
}
: {
sort: sort
}
{
limit: ATTACHEMNTS_LIMIT,
sort: sort
}
)
</script>
@ -75,12 +71,13 @@
</div>
</div>
{/each}
{#if attachmentsLimit && visibleAttachments.length < totalAttachments}
{#if visibleAttachments.length < totalAttachments}
<div
class="showMoreAttachmentsButton"
on:click={() => {
// TODO: replace this with an external attachments page
attachmentsLimit = undefined
const loc = getCurrentLocation()
loc.path[2] = 'fileBrowser'
navigate(loc)
}}
>
<Label label={attachment.string.ShowMoreAttachments} />

View File

@ -55,30 +55,30 @@
{#if channel}
<div class="flex-col flex-gap-3">
{#if isCommonChannel(channel)}
<EditBox
label={chunter.string.Topic}
bind:value={channel.topic}
placeholder={chunter.string.Topic}
maxWidth="39rem"
focus
on:change={onTopicChange}
/>
<EditBox
label={chunter.string.ChannelDescription}
bind:value={channel.description}
placeholder={chunter.string.ChannelDescription}
maxWidth="39rem"
focus
on:change={onDescriptionChange}
/>
<Button
label={chunter.string.LeaveChannel}
justify={'left'}
size={'x-large'}
on:click={() => {
leaveChannel()
}}
/>
<EditBox
label={chunter.string.Topic}
bind:value={channel.topic}
placeholder={chunter.string.Topic}
maxWidth="39rem"
focus
on:change={onTopicChange}
/>
<EditBox
label={chunter.string.ChannelDescription}
bind:value={channel.description}
placeholder={chunter.string.ChannelDescription}
maxWidth="39rem"
focus
on:change={onDescriptionChange}
/>
<Button
label={chunter.string.LeaveChannel}
justify={'left'}
size={'x-large'}
on:click={() => {
leaveChannel()
}}
/>
{/if}
<EditChannelDescriptionAttachments {channel} />
</div>