mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 20:57:45 +02:00
Merge remote-tracking branch 'origin/develop'
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -158,7 +158,7 @@ services:
|
||||
limits:
|
||||
memory: 1024M
|
||||
print:
|
||||
image: hardcoreeng/uberflow-print
|
||||
image: hardcoreeng/print
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4005:4005
|
||||
@@ -171,7 +171,7 @@ services:
|
||||
limits:
|
||||
memory: 300M
|
||||
sign:
|
||||
image: hardcoreeng/uberflow-sign
|
||||
image: hardcoreeng/sign
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4006:4006
|
||||
@@ -193,7 +193,7 @@ services:
|
||||
limits:
|
||||
memory: 300M
|
||||
analytics:
|
||||
image: hardcoreeng/uberflow-analytics-collector
|
||||
image: hardcoreeng/analytics-collector
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4077:4007
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
export let checkForHeaders: boolean = false
|
||||
export let stickedScrollBars: boolean = false
|
||||
export let thinScrollBars: boolean = false
|
||||
export let disableOverscroll = false
|
||||
export let onScroll: ((params: ScrollParams) => void) | undefined = undefined
|
||||
export let onResize: (() => void) | undefined = undefined
|
||||
|
||||
@@ -542,6 +543,7 @@
|
||||
onResize?.()
|
||||
}}
|
||||
class="scroll relative flex-shrink"
|
||||
class:disableOverscroll
|
||||
style:overflow-x={horizontal ? 'auto' : 'hidden'}
|
||||
on:scroll={() => {
|
||||
if (onScroll) {
|
||||
@@ -838,6 +840,9 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
&.disableOverscroll {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
&::-webkit-scrollbar:vertical {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ interface IChannelDataProvider {
|
||||
datesStore: Readable<Timestamp[]>
|
||||
metadataStore: Readable<MessageMetadata[]>
|
||||
|
||||
loadMore: (mode: LoadMode, loadAfter: Timestamp) => Promise<void>
|
||||
canLoadMore: (mode: LoadMode, loadAfter: Timestamp) => boolean
|
||||
jumpToDate: (date: Timestamp) => Promise<void>
|
||||
}
|
||||
@@ -95,10 +94,27 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
([initialLoaded, tailLoading]) => !initialLoaded || tailLoading
|
||||
)
|
||||
|
||||
private readonly backwardNextStore = writable<Chunk | undefined>(undefined)
|
||||
private readonly forwardNextStore = writable<Chunk | undefined>(undefined)
|
||||
|
||||
private backwardNextPromise: Promise<void> | undefined = undefined
|
||||
private forwardNextPromise: Promise<void> | undefined = undefined
|
||||
|
||||
private readonly isBackwardLoading = writable(false)
|
||||
private readonly isForwardLoading = writable(false)
|
||||
|
||||
private nextChunkAdding = false
|
||||
|
||||
public messagesStore = derived([this.chunksStore, this.tailStore], ([chunks, tail]) => {
|
||||
return [...chunks.map(({ data }) => data).flat(), ...tail]
|
||||
})
|
||||
|
||||
public canLoadNextForwardStore = derived([this.messagesStore, this.forwardNextStore], ([messages, forwardNext]) => {
|
||||
if (forwardNext !== undefined) return false
|
||||
|
||||
return this.canLoadMore('forward', messages[messages.length - 1]?.createdOn)
|
||||
})
|
||||
|
||||
constructor (
|
||||
chatId: Ref<Doc>,
|
||||
_class: Ref<Class<ActivityMessage>>,
|
||||
@@ -139,20 +155,15 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
|
||||
private clearData (): void {
|
||||
this.metadataStore.set([])
|
||||
this.tailStore.set([])
|
||||
this.chunksStore.set([])
|
||||
|
||||
this.isInitialLoadingStore.set(false)
|
||||
this.isInitialLoadedStore.set(false)
|
||||
this.isTailLoading.set(false)
|
||||
|
||||
this.datesStore.set([])
|
||||
this.newTimestampStore.set(undefined)
|
||||
this.isLoadingMoreStore.set(false)
|
||||
|
||||
this.tailStart = undefined
|
||||
this.chatId = undefined
|
||||
this.selectedMsgId = undefined
|
||||
|
||||
this.clearMessages()
|
||||
}
|
||||
|
||||
private async loadData (loadAll = false): Promise<void> {
|
||||
@@ -211,9 +222,13 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
this.isTailLoading.set(true)
|
||||
const tailStart = metadata[startIndex]?.createdOn
|
||||
this.loadTail(tailStart)
|
||||
this.backwardNextPromise = this.loadNext('backward', metadata[startIndex]?.createdOn, this.limit)
|
||||
} else {
|
||||
const newStart = Math.max(startPosition - this.limit / 2, 0)
|
||||
await this.loadMore('forward', metadata[newStart]?.createdOn, this.limit)
|
||||
if (newStart > 0) {
|
||||
this.backwardNextPromise = this.loadNext('backward', metadata[newStart]?.createdOn, this.limit)
|
||||
}
|
||||
}
|
||||
|
||||
this.isInitialLoadingStore.set(false)
|
||||
@@ -260,41 +275,28 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
)
|
||||
}
|
||||
|
||||
public async loadMore (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise<void> {
|
||||
if (this.chatId === undefined || loadAfter === undefined) {
|
||||
return
|
||||
}
|
||||
isNextLoading (mode: LoadMode): boolean {
|
||||
return mode === 'forward' ? get(this.isForwardLoading) : get(this.isBackwardLoading)
|
||||
}
|
||||
|
||||
if (!this.canLoadMore(mode, loadAfter) || get(this.isLoadingMoreStore)) {
|
||||
return
|
||||
}
|
||||
isNextLoaded (mode: LoadMode): boolean {
|
||||
return mode === 'forward' ? get(this.forwardNextStore) !== undefined : get(this.backwardNextStore) !== undefined
|
||||
}
|
||||
|
||||
this.isLoadingMoreStore.set(true)
|
||||
setNextLoading (mode: LoadMode, value: boolean): void {
|
||||
mode === 'forward' ? this.isForwardLoading.set(value) : this.isBackwardLoading.set(value)
|
||||
}
|
||||
|
||||
const isBackward = mode === 'backward'
|
||||
const isForward = mode === 'forward'
|
||||
getTailStartIndex (metadata: MessageMetadata[], loadAfter: Timestamp): number {
|
||||
const index = metadata.slice(-this.limit - 1).findIndex(({ createdOn }) => createdOn === loadAfter)
|
||||
|
||||
const chunks = get(this.chunksStore)
|
||||
const tail = get(this.tailStore)
|
||||
const lastChunk: Chunk | undefined = isBackward ? chunks[0] : chunks[chunks.length - 1]
|
||||
const skipIds = (lastChunk?.data ?? [])
|
||||
.concat(tail)
|
||||
.filter(({ createdOn }) => createdOn === loadAfter)
|
||||
.map(({ _id }) => _id) as Array<Ref<ChatMessage>>
|
||||
|
||||
if (isForward) {
|
||||
const metadata = get(this.metadataStore)
|
||||
const metaIndex = metadata.findIndex(({ createdOn }) => createdOn === loadAfter)
|
||||
const shouldLoadTail = metaIndex >= 0 && metaIndex + this.limit >= metadata.length
|
||||
|
||||
if (shouldLoadTail) {
|
||||
this.loadTail(metadata[metaIndex + 1]?.createdOn, undefined, { _id: { $nin: skipIds } })
|
||||
this.isLoadingMoreStore.set(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
return index !== -1 ? metadata.length - index : -1
|
||||
}
|
||||
|
||||
async loadChunk (isBackward: boolean, loadAfter: Timestamp, limit?: number): Promise<Chunk | undefined> {
|
||||
const client = getClient()
|
||||
const skipIds = this.getChunkSkipIds(loadAfter)
|
||||
|
||||
const messages = await client.findAll(
|
||||
chunter.class.ChatMessage,
|
||||
{
|
||||
@@ -312,20 +314,149 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
)
|
||||
|
||||
if (messages.length === 0) {
|
||||
this.isLoadingMoreStore.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
const from = isBackward ? messages[0] : messages[messages.length - 1]
|
||||
const to = isBackward ? messages[messages.length - 1] : messages[0]
|
||||
|
||||
const chunk: Chunk = {
|
||||
return {
|
||||
from: from.createdOn ?? from.modifiedOn,
|
||||
to: to.createdOn ?? to.modifiedOn,
|
||||
data: isBackward ? messages.reverse() : messages
|
||||
}
|
||||
}
|
||||
|
||||
getChunkSkipIds (after: Timestamp, loadTail = false): Array<Ref<ChatMessage>> {
|
||||
const chunks = get(this.chunksStore)
|
||||
const metadata = get(this.metadataStore)
|
||||
const tail = get(this.tailStore)
|
||||
const tailData = tail.length > 0 ? get(this.tailStore) : metadata.slice(-this.limit)
|
||||
|
||||
return chunks
|
||||
.filter(({ to, from }) => from >= after || to <= after)
|
||||
.map(({ data }) => data as MessageMetadata[])
|
||||
.flat()
|
||||
.concat(loadTail ? [] : tailData)
|
||||
.filter(({ createdOn }) => createdOn === after)
|
||||
.map(({ _id }) => _id) as Array<Ref<ChatMessage>>
|
||||
}
|
||||
|
||||
async loadNext (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise<void> {
|
||||
if (this.chatId === undefined || loadAfter === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isNextLoading(mode) || this.isNextLoaded(mode)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.canLoadMore(mode, loadAfter)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setNextLoading(mode, true)
|
||||
|
||||
const isBackward = mode === 'backward'
|
||||
const isForward = mode === 'forward'
|
||||
|
||||
const metadata = get(this.metadataStore)
|
||||
|
||||
if (isForward && this.getTailStartIndex(metadata, loadAfter) !== -1) {
|
||||
this.setNextLoading(mode, false)
|
||||
return
|
||||
}
|
||||
|
||||
const chunk = await this.loadChunk(isBackward, loadAfter, limit)
|
||||
|
||||
if (chunk !== undefined && isBackward) {
|
||||
this.backwardNextStore.set(chunk)
|
||||
}
|
||||
if (chunk !== undefined && isForward) {
|
||||
this.forwardNextStore.set(chunk)
|
||||
}
|
||||
|
||||
this.setNextLoading(mode, false)
|
||||
}
|
||||
|
||||
public async addNextChunk (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise<void> {
|
||||
if (loadAfter === undefined || this.nextChunkAdding) {
|
||||
return
|
||||
}
|
||||
|
||||
this.nextChunkAdding = true
|
||||
|
||||
if (this.forwardNextPromise instanceof Promise && mode === 'forward') {
|
||||
await this.forwardNextPromise
|
||||
this.forwardNextPromise = undefined
|
||||
}
|
||||
|
||||
if (this.backwardNextPromise instanceof Promise && mode === 'backward') {
|
||||
await this.backwardNextPromise
|
||||
this.backwardNextPromise = undefined
|
||||
}
|
||||
|
||||
if (this.isNextLoaded(mode)) {
|
||||
const next = mode === 'forward' ? get(this.forwardNextStore) : get(this.backwardNextStore)
|
||||
if (next !== undefined) {
|
||||
if (mode === 'forward') {
|
||||
this.forwardNextStore.set(undefined)
|
||||
this.chunksStore.set([...get(this.chunksStore), next])
|
||||
this.forwardNextPromise = this.loadNext('forward', next.from, limit)
|
||||
} else {
|
||||
this.backwardNextStore.set(undefined)
|
||||
this.chunksStore.set([next, ...get(this.chunksStore)])
|
||||
this.backwardNextPromise = this.loadNext('backward', next.to, limit)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.loadMore(mode, loadAfter, limit)
|
||||
}
|
||||
|
||||
this.nextChunkAdding = false
|
||||
}
|
||||
|
||||
private async loadMore (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise<void> {
|
||||
if (get(this.isLoadingMoreStore) || loadAfter === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.canLoadMore(mode, loadAfter)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isLoadingMoreStore.set(true)
|
||||
|
||||
const isBackward = mode === 'backward'
|
||||
const isForward = mode === 'forward'
|
||||
|
||||
const chunks = get(this.chunksStore)
|
||||
const metadata = get(this.metadataStore)
|
||||
|
||||
if (isForward) {
|
||||
const index = this.getTailStartIndex(metadata, loadAfter)
|
||||
const tailAfter = metadata[index]?.createdOn
|
||||
|
||||
if (tailAfter !== undefined) {
|
||||
const skipIds = chunks[chunks.length - 1]?.data.map(({ _id }) => _id) ?? []
|
||||
this.loadTail(tailAfter, undefined, { _id: { $nin: skipIds } })
|
||||
this.isLoadingMoreStore.set(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const chunk = await this.loadChunk(isBackward, loadAfter, limit)
|
||||
|
||||
if (chunk !== undefined) {
|
||||
this.chunksStore.set(isBackward ? [chunk, ...chunks] : [...chunks, chunk])
|
||||
|
||||
if (isBackward) {
|
||||
this.forwardNextPromise = this.loadNext('backward', chunk.to, limit)
|
||||
} else {
|
||||
this.forwardNextPromise = this.loadNext('forward', chunk.from, limit)
|
||||
}
|
||||
}
|
||||
|
||||
this.chunksStore.set(isBackward ? [chunk, ...chunks] : [...chunks, chunk])
|
||||
this.isLoadingMoreStore.set(false)
|
||||
}
|
||||
|
||||
@@ -426,6 +557,12 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
this.isInitialLoadedStore.set(false)
|
||||
this.tailQuery.unsubscribe()
|
||||
this.tailStart = undefined
|
||||
this.backwardNextPromise = undefined
|
||||
this.forwardNextPromise = undefined
|
||||
this.forwardNextStore.set(undefined)
|
||||
this.backwardNextStore.set(undefined)
|
||||
this.isBackwardLoading.set(false)
|
||||
this.isForwardLoading.set(false)
|
||||
}
|
||||
|
||||
public async jumpToDate (date: Timestamp): Promise<void> {
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
const dateSelectorHeight = 30
|
||||
const headerHeight = 52
|
||||
const minMsgHeightRem = 2
|
||||
const loadMoreThreshold = 40
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@@ -244,7 +245,7 @@
|
||||
return false
|
||||
}
|
||||
|
||||
return scrollElement.scrollTop === 0
|
||||
return scrollElement.scrollTop <= loadMoreThreshold
|
||||
}
|
||||
|
||||
function shouldLoadMoreDown (): boolean {
|
||||
@@ -254,10 +255,11 @@
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = scrollElement
|
||||
|
||||
return scrollHeight - Math.ceil(scrollTop + clientHeight) <= 0
|
||||
return scrollHeight - Math.ceil(scrollTop + clientHeight) <= loadMoreThreshold
|
||||
}
|
||||
|
||||
let scrollToRestore = 0
|
||||
let backwardRequested = false
|
||||
|
||||
function loadMore (): void {
|
||||
if (!loadMoreAllowed || $isLoadingMoreStore || !scrollElement || isInitialScrolling) {
|
||||
@@ -268,18 +270,24 @@
|
||||
const maxMsgPerScreen = Math.ceil(scrollElement.clientHeight / minMsgHeightPx)
|
||||
const limit = Math.max(maxMsgPerScreen, provider.limit)
|
||||
|
||||
if (shouldLoadMoreUp() && scrollElement && provider.canLoadMore('backward', messages[0]?.createdOn)) {
|
||||
if (!shouldLoadMoreUp()) {
|
||||
backwardRequested = false
|
||||
}
|
||||
|
||||
if (shouldLoadMoreUp() && !backwardRequested) {
|
||||
shouldScrollToNew = false
|
||||
scrollToRestore = scrollElement.scrollHeight
|
||||
void provider.loadMore('backward', messages[0]?.createdOn, limit)
|
||||
} else if (shouldLoadMoreDown() && provider.canLoadMore('forward', messages[messages.length - 1]?.createdOn)) {
|
||||
scrollToRestore = scrollElement?.scrollHeight ?? 0
|
||||
provider.addNextChunk('backward', messages[0]?.createdOn, limit)
|
||||
backwardRequested = true
|
||||
} else if (shouldLoadMoreDown()) {
|
||||
scrollToRestore = 0
|
||||
shouldScrollToNew = false
|
||||
void provider.loadMore('forward', messages[messages.length - 1]?.createdOn, limit)
|
||||
isScrollAtBottom = false
|
||||
provider.addNextChunk('forward', messages[messages.length - 1]?.createdOn, limit)
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll ({ autoScrolling }: ScrollParams): void {
|
||||
async function handleScroll ({ autoScrolling }: ScrollParams): Promise<void> {
|
||||
saveScrollPosition()
|
||||
updateDownButtonVisibility($metadataStore, displayMessages, scrollElement)
|
||||
if (autoScrolling) {
|
||||
@@ -668,6 +676,8 @@
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
const canLoadNextForwardStore = provider.canLoadNextForwardStore
|
||||
</script>
|
||||
|
||||
{#if isLoading}
|
||||
@@ -694,10 +704,11 @@
|
||||
bind:divScroll={scrollElement}
|
||||
bind:divBox={scrollContentBox}
|
||||
noStretch={false}
|
||||
disableOverscroll
|
||||
onScroll={handleScroll}
|
||||
onResize={handleResize}
|
||||
>
|
||||
{#if loadMoreAllowed && provider.canLoadMore('backward', messages[0]?.createdOn)}
|
||||
{#if loadMoreAllowed}
|
||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||
{/if}
|
||||
<slot name="header" />
|
||||
@@ -736,7 +747,7 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if loadMoreAllowed && provider.canLoadMore('forward', messages[messages.length - 1]?.createdOn)}
|
||||
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
||||
<HistoryLoading isLoading={$isLoadingMoreStore} />
|
||||
{/if}
|
||||
</Scroller>
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"Upload": "Upload",
|
||||
"CreateDrive": "Create Drive",
|
||||
"CreateFolder": "Create Folder",
|
||||
"UploadFile": "Upload File",
|
||||
"UploadFolder": "Upload Folder",
|
||||
"UploadFile": "Upload Files",
|
||||
"EditDrive": "Edit Drive",
|
||||
"Rename": "Rename",
|
||||
"Restore": "Restore",
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"Upload": "Subir",
|
||||
"CreateDrive": "Crear unidad",
|
||||
"CreateFolder": "Crear carpeta",
|
||||
"UploadFile": "Subir archivo",
|
||||
"UploadFolder": "Subir carpeta",
|
||||
"UploadFile": "Subir archivos",
|
||||
"EditDrive": "Editar unidad",
|
||||
"Rename": "Renombrar",
|
||||
"Restore": "Restaurar",
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"Upload": "Téléverser",
|
||||
"CreateDrive": "Créer un disque",
|
||||
"CreateFolder": "Créer un dossier",
|
||||
"UploadFile": "Télécharger un fichier",
|
||||
"UploadFolder": "Télécharger un dossier",
|
||||
"UploadFile": "Télécharger des fichiers",
|
||||
"EditDrive": "Modifier le disque",
|
||||
"Rename": "Renommer",
|
||||
"Restore": "Restaurer",
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"Upload": "Carregar",
|
||||
"CreateDrive": "Criar unidade",
|
||||
"CreateFolder": "Criar pasta",
|
||||
"UploadFile": "Carregar ficheiro",
|
||||
"UploadFolder": "Carregar pasta",
|
||||
"UploadFile": "Carregar ficheiros",
|
||||
"EditDrive": "Editar unidade",
|
||||
"Rename": "Renomear",
|
||||
"Restore": "Restaurar",
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"Upload": "Загрузить",
|
||||
"CreateDrive": "Создать диск",
|
||||
"CreateFolder": "Создать папку",
|
||||
"UploadFile": "Загрузить файл",
|
||||
"UploadFolder": "Загрузить папку",
|
||||
"UploadFile": "Загрузить файлы",
|
||||
"EditDrive": "Редактировать",
|
||||
"Rename": "Переименовать",
|
||||
"Restore": "Восстановить",
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"CreateDrive": "创建磁盘",
|
||||
"CreateFolder": "创建文件夹",
|
||||
"UploadFile": "上传文件",
|
||||
"UploadFolder": "上传文件夹",
|
||||
"EditDrive": "编辑磁盘",
|
||||
"Rename": "重命名",
|
||||
"Restore": "恢复",
|
||||
|
||||
@@ -14,22 +14,19 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AccountRole, Ref, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import { createFile, type Drive } from '@hcengineering/drive'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { type Drive } from '@hcengineering/drive'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Button, ButtonWithDropdown, IconAdd, IconDropdown, Loading, SelectPopupValueType } from '@hcengineering/ui'
|
||||
import { showFilesUploadPopup } from '@hcengineering/uploader'
|
||||
|
||||
import drive from '../plugin'
|
||||
import { getFolderIdFromFragment } from '../navigation'
|
||||
import { showCreateDrivePopup, showCreateFolderPopup } from '../utils'
|
||||
import { showCreateDrivePopup, showCreateFolderPopup, uploadFilesToDrivePopup } from '../utils'
|
||||
|
||||
export let currentSpace: Ref<Drive> | undefined
|
||||
export let currentFragment: string | undefined
|
||||
|
||||
const me = getCurrentAccount()
|
||||
|
||||
const client = getClient()
|
||||
const query = createQuery()
|
||||
|
||||
let loading = true
|
||||
@@ -66,27 +63,7 @@
|
||||
|
||||
async function handleUploadFile (): Promise<void> {
|
||||
if (currentSpace !== undefined) {
|
||||
const space = currentSpace
|
||||
const target =
|
||||
parent !== drive.ids.Root
|
||||
? { objectId: parent, objectClass: drive.class.Folder }
|
||||
: { objectId: space, objectClass: drive.class.Drive }
|
||||
await showFilesUploadPopup(target, {}, async (uuid, name, file, path, metadata) => {
|
||||
try {
|
||||
const data = {
|
||||
file: uuid,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
lastModified: file instanceof File ? file.lastModified : Date.now(),
|
||||
name,
|
||||
metadata
|
||||
}
|
||||
|
||||
await createFile(client, space, parent, data)
|
||||
} catch (err) {
|
||||
void setPlatformStatus(unknownError(err))
|
||||
}
|
||||
})
|
||||
await uploadFilesToDrivePopup(currentSpace, parent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,12 +72,10 @@
|
||||
{ id: drive.string.CreateDrive, label: drive.string.CreateDrive, icon: drive.icon.Drive },
|
||||
{ id: drive.string.CreateFolder, label: drive.string.CreateFolder, icon: drive.icon.Folder },
|
||||
{ id: drive.string.UploadFile, label: drive.string.UploadFile, icon: drive.icon.File }
|
||||
// { id: drive.string.UploadFolder, label: drive.string.UploadFolder }
|
||||
]
|
||||
: [
|
||||
{ id: drive.string.CreateFolder, label: drive.string.CreateFolder, icon: drive.icon.Folder },
|
||||
{ id: drive.string.UploadFile, label: drive.string.UploadFile, icon: drive.icon.File }
|
||||
// { id: drive.string.UploadFolder, label: drive.string.UploadFolder }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
maxNumberOfFiles: 1,
|
||||
hideProgress: true
|
||||
},
|
||||
{},
|
||||
async (uuid, name, file, path, metadata) => {
|
||||
const data = {
|
||||
file: uuid,
|
||||
|
||||
@@ -21,7 +21,6 @@ export default mergeIds(driveId, drive, {
|
||||
CreateDrive: '' as IntlString,
|
||||
CreateFolder: '' as IntlString,
|
||||
UploadFile: '' as IntlString,
|
||||
UploadFolder: '' as IntlString,
|
||||
Download: '' as IntlString,
|
||||
Upload: '' as IntlString,
|
||||
EditDrive: '' as IntlString,
|
||||
|
||||
@@ -19,7 +19,12 @@ import drive, { createFile } from '@hcengineering/drive'
|
||||
import { type Asset, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type AnySvelteComponent, showPopup } from '@hcengineering/ui'
|
||||
import { uploadFiles } from '@hcengineering/uploader'
|
||||
import {
|
||||
type FileUploadCallback,
|
||||
getDataTransferFiles,
|
||||
showFilesUploadPopup,
|
||||
uploadFiles
|
||||
} from '@hcengineering/uploader'
|
||||
import { openDoc } from '@hcengineering/view-resources'
|
||||
|
||||
import CreateDrive from './components/CreateDrive.svelte'
|
||||
@@ -154,7 +159,38 @@ export async function resolveParents (object: Resource): Promise<Doc[]> {
|
||||
return parents.reverse()
|
||||
}
|
||||
|
||||
export async function uploadFilesToDrive (files: DataTransfer, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
|
||||
export async function uploadFilesToDrive (dt: DataTransfer, space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
|
||||
const files = await getDataTransferFiles(dt)
|
||||
|
||||
const onFileUploaded = await fileUploadCallback(space, parent)
|
||||
|
||||
const target =
|
||||
parent !== drive.ids.Root
|
||||
? { objectId: parent, objectClass: drive.class.Folder }
|
||||
: { objectId: space, objectClass: drive.class.Drive }
|
||||
|
||||
await uploadFiles(files, target, {}, onFileUploaded)
|
||||
}
|
||||
|
||||
export async function uploadFilesToDrivePopup (space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
|
||||
const onFileUploaded = await fileUploadCallback(space, parent)
|
||||
|
||||
const target =
|
||||
parent !== drive.ids.Root
|
||||
? { objectId: parent, objectClass: drive.class.Folder }
|
||||
: { objectId: space, objectClass: drive.class.Drive }
|
||||
|
||||
await showFilesUploadPopup(
|
||||
target,
|
||||
{},
|
||||
{
|
||||
fileManagerSelectionType: 'both'
|
||||
},
|
||||
onFileUploaded
|
||||
)
|
||||
}
|
||||
|
||||
async function fileUploadCallback (space: Ref<Drive>, parent: Ref<Folder>): Promise<FileUploadCallback> {
|
||||
const client = getClient()
|
||||
|
||||
const query = parent !== drive.ids.Root ? { space, path: parent } : { space }
|
||||
@@ -190,12 +226,7 @@ export async function uploadFilesToDrive (files: DataTransfer, space: Ref<Drive>
|
||||
return current
|
||||
}
|
||||
|
||||
const target =
|
||||
parent !== drive.ids.Root
|
||||
? { objectId: parent, objectClass: drive.class.Folder }
|
||||
: { objectId: space, objectClass: drive.class.Drive }
|
||||
|
||||
await uploadFiles(files, target, {}, async (uuid, name, file, path, metadata) => {
|
||||
const callback: FileUploadCallback = async (uuid, name, file, path, metadata) => {
|
||||
const folder = await findParent(path)
|
||||
try {
|
||||
const data = {
|
||||
@@ -211,5 +242,7 @@ export async function uploadFilesToDrive (files: DataTransfer, space: Ref<Drive>
|
||||
} catch (err) {
|
||||
void setPlatformStatus(unknownError(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return callback
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
import { type FileUploadPopupOptions } from '@hcengineering/uploader'
|
||||
|
||||
import { type Uppy } from '@uppy/core'
|
||||
import Dashboard from '@uppy/dashboard'
|
||||
@@ -25,6 +26,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let uppy: Uppy<any, any>
|
||||
export let options: FileUploadPopupOptions
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
@@ -46,7 +48,8 @@
|
||||
width: 750,
|
||||
disableInformer: true,
|
||||
proudlyDisplayPoweredByUppy: false,
|
||||
theme: dark ? 'dark' : 'light'
|
||||
theme: dark ? 'dark' : 'light',
|
||||
fileManagerSelectionType: options.fileManagerSelectionType
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
class="container flex-row-center flex-gap-2 active"
|
||||
class:error={state.error}
|
||||
on:click={handleClick}
|
||||
use:tooltip={state.error != null ? { label: getEmbeddedLabel(state.error) } : undefined}
|
||||
use:tooltip={state.error !== undefined ? { label: getEmbeddedLabel(state.error) } : undefined}
|
||||
>
|
||||
{#if state.error}
|
||||
<IconError size={'small'} fill={'var(--negative-button-default)'} />
|
||||
|
||||
@@ -70,6 +70,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelAll (): void {
|
||||
upload.uppy.cancelAll()
|
||||
}
|
||||
|
||||
function handleCancelFile (file: UppyFile<any, any>): void {
|
||||
upload.uppy.removeFile(file.id)
|
||||
}
|
||||
@@ -123,6 +127,17 @@
|
||||
noUnderline
|
||||
/>
|
||||
</div>
|
||||
{#if state.error}
|
||||
<Button
|
||||
kind={'icon'}
|
||||
icon={IconClose}
|
||||
iconProps={{ size: 'small' }}
|
||||
showTooltip={{ label: uploader.string.Cancel }}
|
||||
on:click={() => {
|
||||
handleCancelAll()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<Scroller>
|
||||
<div class="upload-popup__content flex-col flex-no-shrink flex-gap-4">
|
||||
@@ -209,6 +224,8 @@
|
||||
|
||||
.upload-popup__header {
|
||||
padding-bottom: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.625rem;
|
||||
}
|
||||
|
||||
.upload-popup__content {
|
||||
|
||||
@@ -17,8 +17,8 @@ import { showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
type FileUploadCallback,
|
||||
type FileUploadOptions,
|
||||
type FileUploadPopupOptions,
|
||||
type FileUploadTarget,
|
||||
getDataTransferFiles,
|
||||
toFileWithPath
|
||||
} from '@hcengineering/uploader'
|
||||
|
||||
@@ -31,11 +31,12 @@ import { getUppy } from './uppy'
|
||||
export async function showFilesUploadPopup (
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
popupOptions: FileUploadPopupOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
): Promise<void> {
|
||||
const uppy = getUppy(options, onFileUploaded)
|
||||
|
||||
showPopup(FileUploadPopup, { uppy, target }, undefined, (res) => {
|
||||
showPopup(FileUploadPopup, { uppy, target, options: popupOptions }, undefined, (res) => {
|
||||
if (res === true && options.hideProgress !== true) {
|
||||
dockFileUpload(target, uppy)
|
||||
}
|
||||
@@ -44,13 +45,12 @@ export async function showFilesUploadPopup (
|
||||
|
||||
/** @public */
|
||||
export async function uploadFiles (
|
||||
files: File[] | FileList | DataTransfer,
|
||||
files: File[] | FileList,
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
): Promise<void> {
|
||||
const items =
|
||||
files instanceof DataTransfer ? await getDataTransferFiles(files) : Array.from(files, (p) => toFileWithPath(p))
|
||||
const items = Array.from(files, (p) => toFileWithPath(p))
|
||||
|
||||
if (items.length === 0) return
|
||||
|
||||
|
||||
@@ -24,12 +24,13 @@ export interface FileWithPath extends File {
|
||||
export type UploadFilesPopupFn = (
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
popupOptions: FileUploadPopupOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
) => Promise<void>
|
||||
|
||||
/** @public */
|
||||
export type UploadFilesFn = (
|
||||
files: File[] | FileList | DataTransfer,
|
||||
files: File[] | FileList,
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
@@ -49,6 +50,11 @@ export interface FileUploadOptions {
|
||||
hideProgress?: boolean
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface FileUploadPopupOptions {
|
||||
fileManagerSelectionType?: 'files' | 'folders' | 'both'
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type FileUploadCallback = (
|
||||
uuid: Ref<PlatformBlob>,
|
||||
|
||||
@@ -16,21 +16,28 @@
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
|
||||
import uploader from './plugin'
|
||||
import type { FileUploadCallback, FileUploadOptions, FileUploadTarget, FileWithPath } from './types'
|
||||
import type {
|
||||
FileUploadCallback,
|
||||
FileUploadOptions,
|
||||
FileUploadPopupOptions,
|
||||
FileUploadTarget,
|
||||
FileWithPath
|
||||
} from './types'
|
||||
|
||||
/** @public */
|
||||
export async function showFilesUploadPopup (
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
popupOptions: FileUploadPopupOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
): Promise<void> {
|
||||
const fn = await getResource(uploader.function.ShowFilesUploadPopup)
|
||||
await fn(target, options, onFileUploaded)
|
||||
await fn(target, options, popupOptions, onFileUploaded)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export async function uploadFiles (
|
||||
files: File[] | FileList | DataTransfer,
|
||||
files: File[] | FileList,
|
||||
target: FileUploadTarget,
|
||||
options: FileUploadOptions,
|
||||
onFileUploaded: FileUploadCallback
|
||||
@@ -63,6 +70,9 @@ export async function getDataTransferFiles (dataTransfer: DataTransfer): Promise
|
||||
/** @public */
|
||||
export function toFileWithPath (file: File, path?: string): FileWithPath {
|
||||
const { webkitRelativePath } = file
|
||||
if ('relativePath' in file) {
|
||||
return file as FileWithPath
|
||||
}
|
||||
Object.defineProperty(file, 'relativePath', {
|
||||
value:
|
||||
typeof path === 'string'
|
||||
|
||||
@@ -81,9 +81,9 @@ import {
|
||||
type Document,
|
||||
type Filter,
|
||||
type FindCursor,
|
||||
type FindOptions as MongoFindOptions,
|
||||
type Sort,
|
||||
type UpdateFilter,
|
||||
type FindOptions as MongoFindOptions
|
||||
type UpdateFilter
|
||||
} from 'mongodb'
|
||||
import { DBCollectionHelper, getMongoClient, getWorkspaceDB, type MongoClientReference } from './utils'
|
||||
|
||||
@@ -1477,8 +1477,10 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
||||
|
||||
@withContext('get-model')
|
||||
async getModel (ctx: MeasureContext): Promise<Tx[]> {
|
||||
const cursor = await ctx.with('find', {}, async () =>
|
||||
this.db.collection<Tx>(DOMAIN_TX).find(
|
||||
const txCollection = this.db.collection<Tx>(DOMAIN_TX)
|
||||
const exists = await txCollection.indexExists('objectSpace_fi_1__id_fi_1_modifiedOn_fi_1')
|
||||
const cursor = await ctx.with('find', {}, async () => {
|
||||
let c = txCollection.find(
|
||||
{ objectSpace: core.space.Model },
|
||||
{
|
||||
sort: {
|
||||
@@ -1490,7 +1492,11 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter {
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
if (exists) {
|
||||
c = c.hint({ objectSpace: 1, _id: 1, modifiedOn: 1 })
|
||||
}
|
||||
return c
|
||||
})
|
||||
const model = await ctx.with('to-array', {}, async () => await toArray<Tx>(cursor))
|
||||
// We need to put all core.account.System transactions first
|
||||
const systemTx: Tx[] = []
|
||||
|
||||
Reference in New Issue
Block a user