UBERF-7524 Move files and folders in drive (#6016)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-07-08 16:10:42 +07:00 committed by GitHub
parent 8eea8f8ccd
commit fa82ee1939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 177 additions and 4 deletions

View File

@ -45,7 +45,7 @@ import {
import { TDoc, TTypedSpace } from '@hcengineering/model-core' import { TDoc, TTypedSpace } from '@hcengineering/model-core'
import print from '@hcengineering/model-print' import print from '@hcengineering/model-print'
import tracker from '@hcengineering/model-tracker' import tracker from '@hcengineering/model-tracker'
import view, { type Viewlet, createAction } from '@hcengineering/model-view' import view, { type Viewlet, actionTemplates, createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
@ -410,6 +410,23 @@ function defineFolder (builder: Builder): void {
}, },
drive.action.RenameFolder drive.action.RenameFolder
) )
createAction(builder, {
...actionTemplates.move,
action: view.actionImpl.ShowPopup,
actionProps: {
component: drive.component.MoveResource,
element: 'top',
fillProps: {
_object: 'value'
}
},
target: drive.class.Folder,
context: {
mode: ['browser', 'context'],
group: 'tools'
}
})
} }
function defineFile (builder: Builder): void { function defineFile (builder: Builder): void {
@ -488,6 +505,23 @@ function defineFile (builder: Builder): void {
}, },
drive.action.RenameFile drive.action.RenameFile
) )
createAction(builder, {
...actionTemplates.move,
action: view.actionImpl.ShowPopup,
actionProps: {
component: drive.component.MoveResource,
element: 'top',
fillProps: {
_object: 'value'
}
},
target: drive.class.File,
context: {
mode: ['browser', 'context'],
group: 'tools'
}
})
} }
function defineApplication (builder: Builder): void { function defineApplication (builder: Builder): void {

View File

@ -43,6 +43,7 @@ export default mergeIds(driveId, drive, {
FolderPresenter: '' as AnyComponent, FolderPresenter: '' as AnyComponent,
FilePresenter: '' as AnyComponent, FilePresenter: '' as AnyComponent,
FileSizePresenter: '' as AnyComponent, FileSizePresenter: '' as AnyComponent,
MoveResource: '' as AnyComponent,
ResourcePresenter: '' as AnyComponent ResourcePresenter: '' as AnyComponent
}, },
function: { function: {

View File

@ -113,9 +113,6 @@
allowDeselect allowDeselect
showNavigate={false} showNavigate={false}
docProps={{ disabled: true, noUnderline: true }} docProps={{ disabled: true, noUnderline: true }}
on:object={(evt) => {
console.log('selected', evt)
}}
/> />
</svelte:fragment> </svelte:fragment>

View File

@ -0,0 +1,107 @@
<!--
// Copyright © 2024 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 { Ref } from '@hcengineering/core'
import type { Drive, Folder, Resource } from '@hcengineering/drive'
import presentation, { Card, getClient, SpaceSelector } from '@hcengineering/presentation'
import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import drive from '../plugin'
import { moveResources } from '../utils'
import DrivePresenter from './DrivePresenter.svelte'
import ResourcePresenter from './ResourcePresenter.svelte'
export let value: Resource
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let space: Ref<Drive> = value.space
let parent: Ref<Folder> = value.parent as Ref<Folder>
async function save (): Promise<void> {
await moveResources([value], space, parent ?? drive.ids.Root)
}
let children: Ref<Folder>[] = []
$: void updateChildren(value)
async function updateChildren (resource: Resource): Promise<void> {
children = await findChildren(resource)
}
async function findChildren (resource: Resource): Promise<Array<Ref<Folder>>> {
if (hierarchy.isDerived(resource._class, drive.class.Folder)) {
const children = await client.findAll(
drive.class.Folder,
{ space: resource.space, path: resource._id as Ref<Folder> },
{ projection: { _id: 1 } }
)
return children.map((p) => p._id)
}
return []
}
$: canSave = space !== value.space || parent !== value.parent
</script>
<Card
label={view.string.Move}
okAction={save}
okLabel={presentation.string.Save}
fullSize
{canSave}
on:close={() => dispatch('close')}
on:changeContent
>
<svelte:fragment slot="header">
<ResourcePresenter {value} shouldShowAvatar={false} noUnderline />
</svelte:fragment>
<div class="flex-row-center gap-2 min-w-100">
<SpaceSelector
bind:space
_class={drive.class.Drive}
label={drive.string.Drive}
component={DrivePresenter}
iconWithEmoji={view.ids.IconWithEmoji}
defaultIcon={drive.icon.Drive}
kind={'regular'}
size={'small'}
on:change={() => {
parent = drive.ids.Root
}}
/>
<ObjectBox
bind:value={parent}
_class={drive.class.Folder}
label={drive.string.Root}
docQuery={{ space }}
kind={'regular'}
size={'small'}
searchField={'name'}
allowDeselect={true}
showNavigate={false}
docProps={{ disabled: true, noUnderline: true }}
excluded={[value._id, ...children]}
/>
</div>
</Card>

View File

@ -32,6 +32,7 @@ import FileSizePresenter from './components/FileSizePresenter.svelte'
import FolderPanel from './components/FolderPanel.svelte' import FolderPanel from './components/FolderPanel.svelte'
import FolderPresenter from './components/FolderPresenter.svelte' import FolderPresenter from './components/FolderPresenter.svelte'
import GridView from './components/GridView.svelte' import GridView from './components/GridView.svelte'
import MoveResource from './components/MoveResource.svelte'
import ResourcePresenter from './components/ResourcePresenter.svelte' import ResourcePresenter from './components/ResourcePresenter.svelte'
import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation' import { getDriveLink, getFileLink, getFolderLink, resolveLocation } from './navigation'
@ -109,6 +110,7 @@ export default async (): Promise<Resources> => ({
FolderPanel, FolderPanel,
FolderPresenter, FolderPresenter,
GridView, GridView,
MoveResource,
ResourcePresenter ResourcePresenter
}, },
actionImpl: { actionImpl: {

View File

@ -98,6 +98,38 @@ export async function renameResource (resource: Resource): Promise<void> {
}) })
} }
export async function moveResources (resources: Resource[], space: Ref<Drive>, parent: Ref<Folder>): Promise<void> {
const client = getClient()
const folder = parent !== drive.ids.Root ? await client.findOne(drive.class.Folder, { _id: parent }) : undefined
const path = folder !== undefined ? [folder._id, ...folder.path] : []
const folders = resources.filter((p) => p._class === drive.class.Folder).map((p) => p._id)
const children = await client.findAll(drive.class.Resource, { path: { $in: folders } })
const byParent = new Map<Ref<Resource>, Resource[]>()
for (const child of children) {
const group = byParent.get(child.parent) ?? []
group.push(child)
byParent.set(child.parent, group)
}
const ops = client.apply(parent)
for (const resource of resources) {
await ops.update(resource, { space, parent, path })
const children = byParent.get(resource._id) ?? []
for (const child of children) {
// remove old path and add new path
const childPath = [...child.path.filter((p) => !resource.path.includes(p)), ...path]
await ops.update(child, { space, path: childPath })
}
}
await ops.commit()
}
const fileTypesMap: Record<string, AnySvelteComponent> = { const fileTypesMap: Record<string, AnySvelteComponent> = {
'application/pdf': FileTypePdf, 'application/pdf': FileTypePdf,
audio: FileTypeAudio, audio: FileTypeAudio,