Skip to content

Commit

Permalink
[full-ci] Drag & drop on Breadcrumb parent folder (#9052)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* Moving folders works

* Refactor breadcrumb dropped method

* Implement drop hover effect

* Implement edge case error handling: same folder name

* Remove dev leftover

* Implement experimental styling

* remove border, undo experimental breadcrumb styling

* Make drop on root work

* Add unittests for edge case

* Fix rebase errors

* Fix rebase issues

* Fix disable drag/drop on current folder

* Remove unused import / linting

* Implement e2e tests

* Rename dragover class

* Add changelog

* Address PR issues

* Fix unittest

* Fix e2e test for oc10

* Address PR issue

* Set overflow visible

* Fix buttons having different font feature settings

* Address PR issue

* Refactor AppBar drop logic into GenericSpace

* Remove unused import

* Address PR issues

* remove unused resize event listener

* Make linter happy
  • Loading branch information
lookacat committed May 31, 2023
1 parent 37f67c9 commit 16d2d20
Show file tree
Hide file tree
Showing 15 changed files with 207 additions and 30 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-drag-drop-parent-folder
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Drag & drop on parent folder

We've added the possibility to drag & drop files onto the breadcrumb to move items into parent folders in a fast and intuitive way.

https://github.com/owncloud/web/pull/9052
https://github.com/owncloud/web/issues/9043
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
(item.isTruncationPlaceholder && hiddenItems.length === 0)
}
]"
@dragover.prevent
@dragenter.prevent="dropItemStyling(item, index, false, $event)"
@dragleave.prevent="dropItemStyling(item, index, true, $event)"
@mouseleave="dropItemStyling(item, index, true, $event)"
@drop="dropItemEvent(item, index)"
>
<router-link
v-if="item.to"
Expand Down Expand Up @@ -98,7 +103,7 @@
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { AVAILABLE_SIZES } from '../../helpers/constants'
import { AVAILABLE_SIZES, EVENT_ITEM_DROPPED_BREADCRUMB } from '../../helpers/constants'
import OcButton from '../OcButton/OcButton.vue'
import OcDrop from '../OcDrop/OcDrop.vue'
Expand Down Expand Up @@ -190,7 +195,8 @@ export default defineComponent({
default: false
}
},
setup(props) {
emits: [EVENT_ITEM_DROPPED_BREADCRUMB],
setup(props, { emit }) {
const { $gettext } = useGettext()
const visibleItems = ref<BreadcrumbItem[]>([])
const hiddenItems = ref<BreadcrumbItem[]>([])
Expand All @@ -200,6 +206,22 @@ export default defineComponent({
return document.querySelector(`.oc-breadcrumb-list [data-item-id="${id}"]`)
}
const isDropAllowed = (item: BreadcrumbItem, index: number): boolean => {
return !(
!item.id ||
index === unref(displayItems).length - 1 ||
item.isTruncationPlaceholder ||
item.isStaticNav
)
}
const dropItemEvent = (item, index) => {
if (!isDropAllowed(item, index)) {
return
}
item.to.path = item.to.path || '/'
emit(EVENT_ITEM_DROPPED_BREADCRUMB, item.to)
}
const calculateTotalBreadcrumbWidth = () => {
let totalBreadcrumbWidth = 100 // 100px margin to the right to avoid breadcrumb from getting too close to the controls
visibleItems.value.forEach((item) => {
Expand Down Expand Up @@ -244,7 +266,6 @@ export default defineComponent({
}
visibleItems.value = [...displayItems.value]
hiddenItems.value = []
nextTick(() => {
reduceBreadcrumb(props.truncationOffset)
})
Expand All @@ -270,6 +291,21 @@ export default defineComponent({
return props.items.length - 1 === index ? 'page' : null
}
const dropItemStyling = (item: BreadcrumbItem, index: number, leaving: boolean, event) => {
if (!isDropAllowed(item, index)) {
return
}
const hasFilePayload = (event.dataTransfer?.types || []).some((e) => e === 'Files')
if (hasFilePayload) return
if (event.currentTarget?.contains(event.relatedTarget)) {
return
}
const classList = getBreadcrumbElement(item.id).children[0].classList
const className = 'oc-breadcrumb-item-dragover'
leaving ? classList.remove(className) : classList.add(className)
}
return {
currentFolder,
parentFolderTo,
Expand All @@ -279,15 +315,23 @@ export default defineComponent({
hiddenItems,
renderBreadcrumb,
displayItems,
lastHiddenItem
lastHiddenItem,
dropItemEvent,
dropItemStyling
}
}
})
</script>

<style lang="scss">
.oc-breadcrumb {
overflow: hidden;
overflow: visible;
&-item-dragover {
transition: background 0.06s, border 0s 0.08s, border-color 0s, border-width 0.06s;
background-color: var(--oc-color-background-highlight);
box-shadow: 0 0 0 5px var(--oc-color-background-highlight);
border-radius: 5px;
}
&-item-text {
max-width: 200px;
white-space: nowrap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface BreadcrumbItem {
allowContextActions?: boolean
onClick?: () => void
isTruncationPlaceholder?: boolean
isStaticNav?: boolean
}
1 change: 1 addition & 0 deletions packages/design-system/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const EVENT_TROW_CLICKED = 'highlight' as const
export const EVENT_TROW_MOUNTED = 'rowMounted' as const
export const EVENT_TROW_CONTEXTMENU = 'contextmenuClicked' as const
export const EVENT_ITEM_DROPPED = 'itemDropped' as const
export const EVENT_ITEM_DROPPED_BREADCRUMB = 'itemDroppedBreadcrumb' as const
export const EVENT_ITEM_DRAGGED = 'itemDragged' as const
export const EVENT_FILE_DROPPED = 'fileDropped' as const
export const EVENT_SORT = 'sort' as const
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/styles/theme/oc-text.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ html * {
font-family: var(--oc-font-family);
}

html {
html, button {
font-feature-settings: "cv11";
font-size: var(--oc-font-size-default);
}
Expand Down
13 changes: 10 additions & 3 deletions packages/web-app-files/src/components/AppBar/AppBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
:items="breadcrumbs"
:max-width="breadcrumbMaxWidth"
:truncation-offset="breadcrumbTruncationOffset"
@item-dropped-breadcrumb="fileDroppedBreadcrumb"
>
<template #contextMenu>
<context-actions
Expand Down Expand Up @@ -94,9 +95,10 @@ import {
useFileActionsMove,
useFileActionsRestore
} from 'web-app-files/src/composables/actions'
import { useRouter, useStore } from 'web-pkg/src'
import { useClientService, useRouter, useStore } from 'web-pkg/src'
import { BreadcrumbItem } from 'design-system/src/components/OcBreadcrumb/types'
import { useActiveLocation } from 'web-app-files/src/composables'
import { EVENT_ITEM_DROPPED } from 'design-system/src/helpers'
export default defineComponent({
components: {
Expand Down Expand Up @@ -134,9 +136,10 @@ export default defineComponent({
default: null
}
},
setup(props) {
setup(props, { emit }) {
const store = useStore()
const router = useRouter()
const clientService = useClientService()
const { actions: acceptShareActions } = useFileActionsAcceptShare({ store })
const { actions: clearSelectionActions } = useFileActionsClearSelection({ store })
Expand Down Expand Up @@ -200,13 +203,17 @@ export default defineComponent({
? 3
: 2
})
const fileDroppedBreadcrumb = async (data) => {
emit(EVENT_ITEM_DROPPED, data)
}
return {
batchActions,
showBreadcrumb,
showMobileNav,
breadcrumbMaxWidth,
breadcrumbTruncationOffset
breadcrumbTruncationOffset,
fileDroppedBreadcrumb
}
},
data: function () {
Expand Down
7 changes: 5 additions & 2 deletions packages/web-app-files/src/helpers/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const breadcrumbsFromPath = (
to: {
path: '/' + [...current].splice(0, current.length - resource.length + i + 1).join('/'),
query: omit(currentRoute.query, 'fileId', 'page') // TODO: we need the correct fileId in the query. until we have that we must omit it because otherwise we would correct the path to the one of the (wrong) fileId.
}
},
isStaticNav: false
} as BreadcrumbItem)
)
}
Expand All @@ -35,7 +36,9 @@ export const concatBreadcrumbs = (...items: BreadcrumbItem[]): BreadcrumbItem[]
id: uuidv4(),
allowContextActions: last.allowContextActions,
text: last.text,
onClick: () => eventBus.publish('app.files.list.load')
onClick: () => eventBus.publish('app.files.list.load'),
isTruncationPlaceholder: last.isTruncationPlaceholder,
isStaticNav: last.isStaticNav
}
]
}
17 changes: 16 additions & 1 deletion packages/web-app-files/src/helpers/resource/actions/transfer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Resource } from 'web-client'
import { join } from 'path'
import { basename, join } from 'path'
import { SpaceResource } from 'web-client/src/helpers'
import { ClientService, LoadingService, LoadingTaskCallbackArguments } from 'web-pkg/src/services'
import {
Expand Down Expand Up @@ -128,6 +128,16 @@ export class ResourceTransfer extends ConflictDialog {
)
}

// This is for an edge case if a user moves a subfolder with the same name as the parent folder into the parent of the parent folder (which is not possible because of the backend)
public isOverwritingParentFolder(resource, targetFolder, targetFolderResources) {
if (resource.type !== 'folder') {
return false
}
const folderName = basename(resource.path)
const newPath = join(targetFolder.path, folderName)
return targetFolderResources.some((resource) => resource.path === newPath)
}

private async moveResources(
resolvedConflicts: FileConflict[],
targetFolderResources: Resource[],
Expand All @@ -140,6 +150,7 @@ export class ResourceTransfer extends ConflictDialog {
for (let [i, resource] of this.resourcesToMove.entries()) {
// shallow copy of resources to prevent modifying existing rows
resource = { ...resource }

const hasConflict = resolvedConflicts.some((e) => e.resource.id === resource.id)
let targetName = resource.name
let overwriteTarget = false
Expand All @@ -151,6 +162,10 @@ export class ResourceTransfer extends ConflictDialog {
continue
}
if (resolveStrategy === ResolveStrategy.REPLACE) {
if (this.isOverwritingParentFolder(resource, this.targetFolder, targetFolderResources)) {
errors.push({ resourceName: resource.name })
continue
}
overwriteTarget = true
}
if (resolveStrategy === ResolveStrategy.KEEP_BOTH) {
Expand Down
51 changes: 41 additions & 10 deletions packages/web-app-files/src/views/spaces/GenericSpace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:side-bar-open="sideBarOpen"
:space="space"
:view-modes="viewModes"
@item-dropped="fileDropped"
>
<template #actions="{ limitedScreenSpace }">
<create-and-upload
Expand Down Expand Up @@ -170,7 +171,7 @@ import AppLoadingSpinner from 'web-pkg/src/components/AppLoadingSpinner.vue'
import NoContentMessage from 'web-pkg/src/components/NoContentMessage.vue'
import WhitespaceContextMenu from 'web-app-files/src/components/Spaces/WhitespaceContextMenu.vue'
import Pagination from 'web-pkg/src/components/Pagination.vue'
import { useRoute, useRouteQuery } from 'web-pkg/src/composables'
import { useRoute, useRouteQuery, useClientService } from 'web-pkg/src/composables'
import { useDocumentTitle } from 'web-pkg/src/composables/appDefaults/useDocumentTitle'
import { ImageType } from 'web-pkg/src/constants'
import { VisibilityObserver } from 'web-pkg/src/observer'
Expand Down Expand Up @@ -233,6 +234,7 @@ export default defineComponent({
const { $gettext, $ngettext, interpolate: $gettextInterpolate } = useGettext()
const { getDefaultEditorAction } = useFileActions()
const openWithDefaultAppQuery = useRouteQuery('openWithDefaultApp')
const clientService = useClientService()
let loadResourcesEventToken
const canUpload = computed(() => {
Expand Down Expand Up @@ -285,19 +287,22 @@ export default defineComponent({
rootBreadcrumbItems.push({
id: uuidv4(),
text: $gettext('Spaces'),
to: createLocationSpaces('files-spaces-projects')
to: createLocationSpaces('files-spaces-projects'),
isStaticNav: true
})
} else if (isShareSpaceResource(space)) {
rootBreadcrumbItems.push(
{
id: uuidv4(),
text: $gettext('Shares'),
to: { path: '/files/shares' }
to: { path: '/files/shares' },
isStaticNav: true
},
{
id: uuidv4(),
text: $gettext('Shared with me'),
to: { path: '/files/shares/with-me' }
to: { path: '/files/shares/with-me' },
isStaticNav: true
}
)
}
Expand Down Expand Up @@ -331,7 +336,8 @@ export default defineComponent({
to: createLocationPublic('files-public-link', {
params,
query
})
}),
isStaticNav: true
}
} else {
spaceBreadcrumbItem = {
Expand Down Expand Up @@ -489,7 +495,8 @@ export default defineComponent({
uploadHint: $gettext(
'Drag files and folders here or use the "New" or "Upload" buttons to add files'
),
whitespaceContextMenu
whitespaceContextMenu,
clientService
}
},
Expand Down Expand Up @@ -552,11 +559,35 @@ export default defineComponent({
...mapActions(['showMessage', 'createModal', 'hideModal']),
...mapMutations('Files', ['REMOVE_FILES', 'REMOVE_FILES_FROM_SEARCHED', 'RESET_SELECTION']),
async fileDropped(fileIdTarget) {
async fileDropped(fileTarget) {
const selected = [...this.selectedResources]
const targetFolder = this.paginatedResources.find((e) => e.id === fileIdTarget)
const isTargetSelected = selected.some((e) => e.id === fileIdTarget)
if (isTargetSelected) {
let targetFolder = null
if (typeof fileTarget === 'string') {
targetFolder = this.paginatedResources.find((e) => e.id === fileTarget)
const isTargetSelected = selected.some((e) => e.id === fileTarget)
if (isTargetSelected) {
return
}
} else if (fileTarget instanceof Object) {
const spaceRootRoutePath = this.$router.resolve(
createLocationSpaces('files-spaces-generic', {
params: {
driveAliasAndItem: this.space.driveAlias
}
})
).path
const splitIndex = fileTarget.path.indexOf(spaceRootRoutePath) + spaceRootRoutePath.length
const path = decodeURIComponent(fileTarget.path.slice(splitIndex, fileTarget.path.length))
try {
targetFolder = await this.clientService.webdav.getFileInfo(this.space, { path })
} catch (e) {
console.error(e)
return
}
}
if (!targetFolder) {
return
}
if (targetFolder.type !== 'folder') {
Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ export default defineComponent({
return [
{
text: this.$gettext('Spaces'),
onClick: () => this.loadResourcesTask.perform()
onClick: () => this.loadResourcesTask.perform(),
isStativNav: true
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('builds an array of breadcrumbitems', () => {
expect(breadCrumbs).toEqual([
{
id: expect.anything(),
isStaticNav: false,
allowContextActions: true,
text: 'test',
to: { path: '/files/spaces/personal/home/test', query: {} }
Expand Down
Loading

0 comments on commit 16d2d20

Please sign in to comment.