Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[full-ci] Drag & drop on Breadcrumb parent folder #9052

Merged
merged 30 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d630359
WIP
lookacat May 16, 2023
316bf86
WIP
lookacat May 16, 2023
07dc25f
Moving folders works
lookacat May 16, 2023
1c13e2f
Refactor breadcrumb dropped method
lookacat May 16, 2023
1b70344
Implement drop hover effect
lookacat May 16, 2023
b31887f
Implement edge case error handling: same folder name
lookacat May 17, 2023
9d3f042
Remove dev leftover
lookacat May 17, 2023
a4e332a
Implement experimental styling
lookacat May 17, 2023
2bde919
remove border, undo experimental breadcrumb styling
lookacat May 17, 2023
28f7bfb
Make drop on root work
lookacat May 17, 2023
5949692
Add unittests for edge case
lookacat May 17, 2023
77a1fa1
Fix rebase errors
lookacat May 26, 2023
31b2819
Fix rebase issues
lookacat May 26, 2023
92c42c9
Fix disable drag/drop on current folder
lookacat May 26, 2023
4527951
Remove unused import / linting
lookacat May 26, 2023
5143c3c
Implement e2e tests
lookacat May 26, 2023
c2f0994
Rename dragover class
lookacat May 26, 2023
075561b
Add changelog
lookacat May 26, 2023
14b1f3b
Address PR issues
lookacat May 30, 2023
95d10a9
Fix unittest
lookacat May 30, 2023
2ad0eda
Fix e2e test for oc10
lookacat May 30, 2023
5d2b905
Address PR issue
lookacat May 30, 2023
df7c348
Set overflow visible
lookacat May 30, 2023
7a47a32
Fix buttons having different font feature settings
lookacat May 31, 2023
634bc78
Address PR issue
lookacat May 31, 2023
f775e06
Refactor AppBar drop logic into GenericSpace
lookacat May 31, 2023
2d94e06
Remove unused import
lookacat May 31, 2023
9c13573
Address PR issues
lookacat May 31, 2023
937194e
remove unused resize event listener
lookacat May 31, 2023
50375f4
Make linter happy
lookacat May 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -95,10 +100,20 @@
</template>

<script lang="ts">
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue'
import {
computed,
defineComponent,
nextTick,
onBeforeUnmount,
onMounted,
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 +205,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 +216,25 @@ export default defineComponent({
return document.querySelector(`.oc-breadcrumb-list [data-item-id="${id}"]`)
}

const isDropAllowed = (item, index): boolean => {
lookacat marked this conversation as resolved.
Show resolved Hide resolved
if (
!item.id ||
index === unref(displayItems).length - 1 ||
item.isTruncationPlaceholder ||
item.isStaticNav
lookacat marked this conversation as resolved.
Show resolved Hide resolved
) {
return false
}
return true
}
const dropItemEvent = (item, index) => {
lookacat marked this conversation as resolved.
Show resolved Hide resolved
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,13 +279,19 @@ export default defineComponent({
}
visibleItems.value = [...displayItems.value]
hiddenItems.value = []

nextTick(() => {
reduceBreadcrumb(props.truncationOffset)
})
}

watch([() => props.maxWidth, () => props.items], renderBreadcrumb, { immediate: true })
onMounted(() => {
window.addEventListener('resize', renderBreadcrumb)
})

onBeforeUnmount(() => {
window.removeEventListener('resize', renderBreadcrumb)
})
lookacat marked this conversation as resolved.
Show resolved Hide resolved

const currentFolder = computed<BreadcrumbItem>(() => {
if (props.items.length === 0 || !props.items) {
Expand All @@ -270,6 +311,21 @@ export default defineComponent({
return props.items.length - 1 === index ? 'page' : null
}

const dropItemStyling = (item, index, leaving, 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 +335,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 an 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)
lookacat marked this conversation as resolved.
Show resolved Hide resolved
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
Loading