Skip to content

Commit

Permalink
[full-ci] Enhancement: Implement Copy/Move Conflict Dialog (#6994)
Browse files Browse the repository at this point in the history
Introduce conflict resolve dialog for move

Co-authored-by: Pascal Wengerter <[email protected]>
  • Loading branch information
lookacat and pascalwengerter authored Jun 3, 2022
1 parent df0540a commit 7759ce4
Show file tree
Hide file tree
Showing 13 changed files with 459 additions and 199 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-copy-move-conflict-dialog
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Copy/Move conflict dialog

We've added a conflict dialog for moving resources via drag&drop in the files list

https://github.com/owncloud/web/pull/6994
https://github.com/owncloud/web/issues/6996
6 changes: 4 additions & 2 deletions changelog/unreleased/enhancement-update-sdk
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Enhancement: Update SDK

We've updated the ownCloud SDK to version 3.0.0-alpha.9.
We've updated the ownCloud SDK to version 3.0.0-alpha.10.

- Change - Pass full trash bin path to methods of FilesTrash class: https://github.com/owncloud/owncloud-sdk/pull/1021
- Enhancement - Enforce share_type guest if applies: https://github.com/owncloud/owncloud-sdk/pull/1046
Expand All @@ -10,9 +10,11 @@ We've updated the ownCloud SDK to version 3.0.0-alpha.9.
- Enhancement - Adjust share management to properly work with spaces: https://github.com/owncloud/owncloud-sdk/pull/1013
- Enhancement - Send oc-etag on putFileContents and getFileContents methods: https://github.com/owncloud/owncloud-sdk/pull/1067
- Enhancement - Enable search results for ocis: https://github.com/owncloud/owncloud-sdk/pull/1057
- Enhancement - Add overwrite flag for file move: https://github.com/owncloud/owncloud-sdk/pull/1073
- Bugfix - Always add X-Request-ID: https://github.com/owncloud/owncloud-sdk/pull/1016
- Bugfix - Always add X-Requested-With header: https://github.com/owncloud/owncloud-sdk/pull/1020

https://github.com/owncloud/web/pull/6820
https://github.com/owncloud/web/pull/6952
https://github.com/owncloud/owncloud-sdk/releases/tag/v3.0.0-alpha.9
https://github.com/owncloud/web/pull/6994
https://github.com/owncloud/owncloud-sdk/releases/tag/v3.0.0-alpha.10
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"@cucumber/pretty-formatter": "^1.0.0-alpha.2",
"@noble/hashes": "^1.0.0",
"@playwright/test": "^1.21.1",
"@rollup-extras/plugin-copy": "^1.2.3",
"@rollup-extras/plugin-copy": "^1.2.5",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-html": "^0.2.4",
Expand All @@ -67,7 +67,7 @@
"@types/node-fetch": "^2.6.0",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"@vue/runtime-dom": "^3.2.31",
"@vue/runtime-dom": "^3.2.36",
"@vue/test-utils": "^1.3.0",
"autoprefixer": "^10.4.2",
"babel-core": "^7.0.0-bridge.0",
Expand Down
1 change: 1 addition & 0 deletions packages/web-app-files/src/helpers/resource/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './privatePreviewBlob'
export * from './publicPreviewUrl'
export * from './resource'
export * from './sameResource'
export * from './move'
223 changes: 223 additions & 0 deletions packages/web-app-files/src/helpers/resource/move.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { Resource } from './index'
import { join } from 'path'

export enum ResolveStrategy {
SKIP,
REPLACE,
KEEP_BOTH
}
export interface ResolveConflict {
strategy: ResolveStrategy
doForAllConflicts: boolean
}
export interface FileConflict {
resource: Resource
strategy?: ResolveStrategy
}

export const resolveFileExists = (
createModal,
hideModal,
resource,
conflictCount,
$gettext,
$gettextInterpolate,
isSingleConflict
) => {
return new Promise<ResolveConflict>((resolve) => {
let doForAllConflicts = false
const modal = {
variation: 'danger',
title: $gettext('File already exists'),
message: $gettextInterpolate(
$gettext('Resource with name %{name} already exists.'),
{ name: resource.name },
true
),
cancelText: $gettext('Skip'),
confirmText: $gettext('Keep both'),
buttonSecondaryText: $gettext('Replace'),
checkboxLabel: isSingleConflict
? ''
: $gettextInterpolate(
$gettext('Do this for all %{count} conflicts'),
{ count: conflictCount },
true
),
onCheckboxValueChanged: (value) => {
doForAllConflicts = value
},
onCancel: () => {
hideModal()
resolve({ strategy: ResolveStrategy.SKIP, doForAllConflicts } as ResolveConflict)
},
onConfirmSecondary: () => {
hideModal()
const strategy = ResolveStrategy.REPLACE
resolve({ strategy, doForAllConflicts } as ResolveConflict)
},
onConfirm: () => {
hideModal()
resolve({ strategy: ResolveStrategy.KEEP_BOTH, doForAllConflicts } as ResolveConflict)
}
}
createModal(modal)
})
}
export const resolveAllConflicts = async (
resourcesToMove,
targetFolder,
client,
createModal,
hideModal,
$gettext,
$gettextInterpolate,
resolveFileExistsMethod
) => {
// if we implement MERGE, we need to use 'infinity' instead of 1
const targetFolderItems = await client.files.list(targetFolder.webDavPath, 1)
const targetPath = targetFolder.path
const index = targetFolder.webDavPath.lastIndexOf(targetPath)
const webDavPrefix = targetFolder.webDavPath.substring(0, index)

// Collect all conflicting resources
const allConflicts = []
for (const resource of resourcesToMove) {
const potentialTargetWebDavPath = join(webDavPrefix, targetFolder.path, resource.name)
const exists = targetFolderItems.some((e) => e.name === potentialTargetWebDavPath)
if (exists) {
allConflicts.push({
resource,
strategy: null
} as FileConflict)
}
}
let count = 0
let doForAllConflicts = false
let doForAllConflictsStrategy = null
const resolvedConflicts = []
for (const conflict of allConflicts) {
// Resolve conflicts accordingly
if (doForAllConflicts) {
conflict.strategy = doForAllConflictsStrategy
resolvedConflicts.push(conflict)
continue
}

// Resolve next conflict
const conflictsLeft = allConflicts.length - count
const result: ResolveConflict = await resolveFileExistsMethod(
createModal,
hideModal,
conflict.resource,
conflictsLeft,
$gettext,
$gettextInterpolate,
allConflicts.length === 1
)
conflict.strategy = result.strategy
resolvedConflicts.push(conflict)
count += 1

// User checked 'do for all x conflicts'
if (!result.doForAllConflicts) continue
doForAllConflicts = true
doForAllConflictsStrategy = result.strategy
}
return resolvedConflicts
}
export const showResultMessage = async (
errors,
movedResources,
showMessage,
$gettext,
$gettextInterpolate,
$ngettext
) => {
if (errors.length === 0) {
const count = movedResources.length
const ntitle = $ngettext(
'%{count} item was moved successfully',
'%{count} items were moved successfully',
count
)
const title = $gettextInterpolate(ntitle, { count }, true)
showMessage({
title,
status: 'success'
})
return
}
let title = $gettextInterpolate(
$gettext('Failed to move %{count} resources'),
{ count: errors.length },
true
)
if (errors.length === 1) {
title = $gettextInterpolate(
$gettext('Failed to move "%{name}"'),
{ name: errors[0]?.resourceName },
true
)
}
showMessage({
title,
status: 'danger'
})
}
export const move = async (
resourcesToMove,
targetFolder,
client,
createModal,
hideModal,
showMessage,
$gettext,
$gettextInterpolate,
$ngettext
) => {
const errors = []
const resolvedConflicts = await resolveAllConflicts(
resourcesToMove,
targetFolder,
client,
createModal,
hideModal,
$gettext,
$gettextInterpolate,
resolveFileExists
)
const movedResources = []

for (const resource of resourcesToMove) {
const hasConflict = resolvedConflicts.some((e) => e.resource.id === resource.id)
let targetName = resource.name
let overwriteTarget = false
if (hasConflict) {
const resolveStrategy = resolvedConflicts.find((e) => e.resource.id === resource.id)?.strategy
if (resolveStrategy === ResolveStrategy.SKIP) {
continue
}
if (resolveStrategy === ResolveStrategy.REPLACE) {
overwriteTarget = true
}
if (resolveStrategy === ResolveStrategy.KEEP_BOTH) {
targetName = $gettextInterpolate($gettext('%{name} copy'), { name: resource.name }, true)
}
}
try {
await client.files.move(
resource.webDavPath,
join(targetFolder.webDavPath, targetName),
overwriteTarget
)
movedResources.push(resource)
} catch (error) {
console.error(error)
error.resourceName = resource.name
errors.push(error)
}
}
showResultMessage(errors, movedResources, showMessage, $gettext, $gettextInterpolate, $ngettext)
return movedResources
}
90 changes: 17 additions & 73 deletions packages/web-app-files/src/views/Personal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,10 @@ import NotFoundMessage from '../components/FilesList/NotFoundMessage.vue'
import ListInfo from '../components/FilesList/ListInfo.vue'
import Pagination from '../components/FilesList/Pagination.vue'
import ContextActions from '../components/FilesList/ContextActions.vue'
import { basename, join } from 'path'
import PQueue from 'p-queue'
import { createLocationSpaces } from '../router'
import { useResourcesViewDefaults } from '../composables'
import { fetchResources } from '../services/folder'
import { defineComponent } from '@vue/composition-api'
import { Resource } from '../helpers/resource'
import { Resource, move } from '../helpers/resource'
import { useCapabilityShareJailEnabled } from 'web-pkg/src/composables'
const visibilityObserver = new VisibilityObserver()
Expand Down Expand Up @@ -221,84 +218,31 @@ export default defineComponent({
methods: {
...mapActions('Files', ['loadPreview']),
...mapActions(['showMessage']),
...mapActions(['showMessage', 'createModal', 'hideModal']),
...mapMutations('Files', ['REMOVE_FILE', 'REMOVE_FILE_FROM_SEARCHED', 'REMOVE_FILE_SELECTION']),
fetchResources,
async fileDropped(fileIdTarget) {
const selected = [...this.selectedResources]
const targetInfo = this.paginatedResources.find((e) => e.id === fileIdTarget)
const isTargetSelected = selected.some((e) => e.id === fileIdTarget)
if (isTargetSelected) return
if (targetInfo.type !== 'folder') return
const itemsInTarget = await this.fetchResources(this.$client, targetInfo.webDavPath)
// try to move all selected files
const errors = []
const movePromises = []
const moveQueue = new PQueue({ concurrency: 4 })
selected.forEach((resource) => {
movePromises.push(
moveQueue.add(async () => {
const exists = itemsInTarget.some((e) => basename(e.name) === resource.name)
if (exists) {
const message = this.$gettext('Resource with name %{name} already exists')
errors.push({
resourceName: resource.name,
message: this.$gettextInterpolate(message, { name: resource.name }, true)
})
return
}
try {
await this.$client.files.move(
resource.webDavPath,
join(targetInfo.webDavPath, resource.name)
)
this.REMOVE_FILE(resource)
this.REMOVE_FILE_FROM_SEARCHED(resource)
this.REMOVE_FILE_SELECTION(resource)
} catch (error) {
console.error(error)
error.resourceName = resource.name
errors.push(error)
}
})
)
})
await Promise.all(movePromises)
// show error / success messages
let title
if (errors.length === 0) {
const count = selected.length
title = this.$ngettext(
'%{count} item was moved successfully',
'%{count} items were moved successfully',
count
)
this.showMessage({
title: this.$gettextInterpolate(title, { count }),
status: 'success'
})
return
}
if (errors.length === 1) {
title = this.$gettext('Failed to move "%{resourceName}"')
this.showMessage({
title: this.$gettextInterpolate(title, { resourceName: errors[0]?.resourceName }, true),
status: 'danger'
})
return
const movedResources = await move(
selected,
targetInfo,
this.$client,
this.createModal,
this.hideModal,
this.showMessage,
this.$gettext,
this.$gettextInterpolate,
this.$ngettext
)
for (const resource of movedResources) {
this.REMOVE_FILE(resource)
this.REMOVE_FILE_FROM_SEARCHED(resource)
this.REMOVE_FILE_SELECTION(resource)
}
title = this.$gettext('Failed to move %{count} resources')
this.showMessage({
title: this.$gettextInterpolate(title, { count: errors.length }),
status: 'danger'
})
},
rowMounted(resource, component) {
Expand Down
Loading

0 comments on commit 7759ce4

Please sign in to comment.