Skip to content

Commit 365adde

Browse files
committed
fix: conflict picker
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent 4d7ea13 commit 365adde

File tree

7 files changed

+844
-766
lines changed

7 files changed

+844
-766
lines changed

lib/components/ConflictPicker.vue

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22
<NcDialog class="conflict-picker"
33
data-cy-conflict-picker
44
:close-on-click-outside="false"
5-
:can-close="false"
5+
:can-close="true"
66
:show="opened"
77
:name="name"
88
size="large"
9-
@close="onCancel">
9+
@closing="onCancel">
1010
<!-- Header -->
1111
<div class="conflict-picker__header">
1212
<!-- Description -->
1313
<p id="conflict-picker-description" class="conflict-picker__description">
1414
{{ t('Which files do you want to keep?') }}<br>
15-
{{ t('If you select both versions, the copied file will have a number added to its name.') }}
15+
{{ t('If you select both versions, the copied file will have a number added to its name.') }}<br>
16+
{{ t('When an incoming folder is selected, any conflicting files within it will also be overwritten.') }}
1617
</p>
1718
</div>
1819

@@ -49,17 +50,35 @@
4950

5051
<!-- Controls -->
5152
<template #actions>
52-
<NcButton data-cy-conflict-picker-skip @click="onSkip">
53+
<!-- Cancel the entire operation -->
54+
<NcButton :aria-label="t('Cancel')"
55+
:title="t('Cancel the entire operation')"
56+
data-cy-conflict-picker-cancel
57+
type="tertiary"
58+
@click="onCancel">
59+
<template #icon>
60+
<Close :size="20" />
61+
</template>
62+
{{ t('Cancel') }}
63+
</NcButton>
64+
65+
<!-- Align right -->
66+
<span class="dialog__actions-separator" />
67+
68+
<NcButton :aria-label="skipButtonLabel"
69+
data-cy-conflict-picker-skip
70+
@click="onSkip">
5371
<template #icon>
5472
<Close :size="20" />
5573
</template>
5674
{{ skipButtonLabel }}
5775
</NcButton>
58-
<NcButton type="primary"
76+
<NcButton :aria-label="t('Continue')"
5977
:class="{ 'button-vue--disabled': !isEnoughSelected}"
6078
:title="isEnoughSelected ? '' : blockedTitle"
61-
native-type="submit"
6279
data-cy-conflict-picker-submit
80+
native-type="submit"
81+
type="primary"
6382
@click.stop.prevent="onSubmit">
6483
<template #icon>
6584
<ArrowRight :size="20" />
@@ -75,23 +94,23 @@ import type { ConflictResolutionResult } from '../index.ts'
7594
import type { PropType } from 'vue'
7695
7796
import { basename, extname } from 'path'
97+
import { defineComponent } from 'vue'
7898
import { Node } from '@nextcloud/files'
7999
import { showError } from '@nextcloud/dialogs'
80-
import Vue from 'vue'
81100
82-
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
83-
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
84-
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
85101
import ArrowRight from 'vue-material-design-icons/ArrowRight.vue'
86102
import Close from 'vue-material-design-icons/Close.vue'
103+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
104+
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
105+
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
87106
88107
import { n, t } from '../utils/l10n.ts'
89108
import logger from '../utils/logger.ts'
90109
import NodesPicker from './NodesPicker.vue'
91110
92111
export type NodesPickerRef = InstanceType<typeof NodesPicker>
93112
94-
export default Vue.extend({
113+
export default defineComponent({
95114
name: 'ConflictPicker',
96115
97116
components: {
@@ -123,6 +142,8 @@ export default Vue.extend({
123142
},
124143
},
125144
145+
emits: ['cancel', 'submit'],
146+
126147
data() {
127148
return {
128149
// computed list of conflicting files already present in the directory
@@ -207,13 +228,12 @@ export default Vue.extend({
207228
},
208229
},
209230
210-
beforeMount() {
231+
mounted() {
211232
// Using map keep the same order
212233
this.files = this.conflicts.map((conflict: File|Node) => {
213234
const name = (conflict instanceof File) ? conflict.name : conflict.basename
214235
return this.content.find((node: Node) => node.basename === name)
215236
}).filter(Boolean) as Node[]
216-
logger.debug('ConflictPicker initialised', { files: this.files, conflicts: this.conflicts, content: this.content })
217237
218238
if (this.conflicts.length === 0 || this.files.length === 0) {
219239
const error = new Error('ConflictPicker: files and conflicts must not be empty')
@@ -222,10 +242,13 @@ export default Vue.extend({
222242
}
223243
224244
if (this.conflicts.length !== this.files.length) {
225-
const error = new Error('ConflictPicker: files and conflicts must have the same length')
245+
const error = new Error('ConflictPicker: files and conflicts must have the same length. Make sure you filter out non conflicting files from the conflicts array.')
226246
this.onCancel(error)
227247
throw error
228248
}
249+
250+
// Successful initialisation
251+
logger.debug('ConflictPicker initialised', { files: this.files, conflicts: this.conflicts, content: this.content })
229252
},
230253
231254
methods: {
@@ -269,11 +292,15 @@ export default Vue.extend({
269292
toRename.forEach(file => {
270293
const name = (file instanceof File) ? file.name : file.basename
271294
const newName = this.getUniqueName(name, directoryContent)
295+
// If File, create a new one with the new name
272296
if (file instanceof File) {
273-
file = new File([file], newName, { type: file.type, lastModified: file.lastModified })
297+
// Keep the original file object and force rename
298+
Object.defineProperty(file, 'name', { value: newName })
274299
renamed.push(file)
275300
return
276301
}
302+
303+
// Rename the node
277304
file.rename(newName)
278305
renamed.push(file)
279306
})
@@ -373,17 +400,15 @@ export default Vue.extend({
373400
z-index: 10;
374401
top: 0;
375402
padding: 0 var(--margin);
376-
padding-bottom: 0;
403+
padding-bottom: var(--secondary-margin);
377404
}
378405
379406
&__form {
380407
position: relative;
381408
overflow: auto;
382409
padding: 0 var(--margin);
383-
// 12 px to underlap the header and controls
384-
// and have the gradient background visible
385-
padding-bottom: 12px;
386-
margin-bottom: -12px;
410+
// overlap header bottom padding
411+
margin-top: calc(-1 * var(--secondary-margin));
387412
}
388413
389414
fieldset {
@@ -425,6 +450,14 @@ export default Vue.extend({
425450
opacity: .5;
426451
filter: saturate(.7);
427452
}
453+
454+
:deep(.dialog__actions) {
455+
width: auto;
456+
margin-inline: 12px;
457+
span.dialog__actions-separator {
458+
margin-left: auto;
459+
}
460+
}
428461
}
429462
430463
// Responsive layout
@@ -445,4 +478,16 @@ export default Vue.extend({
445478
}
446479
}
447480
481+
// Responsive layout
482+
@media screen and (max-width: 512px) {
483+
.conflict-picker {
484+
:deep(.dialog__actions) {
485+
flex-wrap: wrap;
486+
span.dialog__actions-separator {
487+
// Make the second row wrap
488+
width: 100%;
489+
}
490+
}
491+
}
492+
}
448493
</style>

lib/components/NodesPicker.vue

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
@update:checked="onUpdateIncomingChecked">
1010
<span class="node-picker node-picker--incoming">
1111
<!-- Icon or preview -->
12-
<FileSvg v-if="!incomingPreview" class="node-picker__icon" :size="48" />
12+
<template v-if="!incomingPreview">
13+
<FolderSvg v-if="isFolder(existing)" class="node-picker__icon" :size="48" />
14+
<FileSvg v-else class="node-picker__icon" :size="48" />
15+
</template>
1316
<img v-else
1417
class="node-picker__preview"
1518
:src="incomingPreview"
@@ -32,7 +35,10 @@
3235
@update:checked="onUpdateExistingChecked">
3336
<span class="node-picker node-picker--existing">
3437
<!-- Icon or preview -->
35-
<FileSvg v-if="!existingPreview" class="node-picker__icon" :size="48" />
38+
<template v-if="!existingPreview">
39+
<FolderSvg v-if="isFolder(existing)" class="node-picker__icon" :size="48" />
40+
<FileSvg v-else class="node-picker__icon" :size="48" />
41+
</template>
3642
<img v-else
3743
class="node-picker__preview"
3844
:src="existingPreview"
@@ -53,33 +59,35 @@
5359
<script lang="ts">
5460
import type { PropType } from 'vue'
5561
62+
import { defineComponent } from 'vue'
5663
import { File as NcFile, Folder, formatFileSize, FileType, Node } from '@nextcloud/files'
5764
import { generateUrl } from '@nextcloud/router'
58-
import moment from 'moment'
59-
import Vue from 'vue'
65+
import moment from '@nextcloud/moment'
6066
6167
import FileSvg from 'vue-material-design-icons/File.vue'
68+
import FolderSvg from 'vue-material-design-icons/Folder.vue'
6269
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
6370
6471
import { t } from '../utils/l10n.ts'
6572
6673
const PREVIEW_SIZE = 64
6774
68-
export default Vue.extend({
75+
export default defineComponent({
6976
name: 'NodesPicker',
7077
7178
components: {
7279
FileSvg,
80+
FolderSvg,
7381
NcCheckboxRadioSwitch,
7482
},
7583
7684
props: {
7785
incoming: {
78-
type: [Node, File, NcFile, Folder],
86+
type: Object as PropType<File|Node|Folder|NcFile>,
7987
required: true,
8088
},
8189
existing: {
82-
type: [NcFile, Folder],
90+
type: Object as PropType<Folder|NcFile>,
8391
required: true,
8492
},
8593
newSelected: {
@@ -166,6 +174,11 @@ export default Vue.extend({
166174
}
167175
},
168176
177+
isFolder(node: File|Node): boolean {
178+
return node.type === FileType.Folder
179+
|| node.type === 'httpd/unix-directory'
180+
},
181+
169182
isChecked(node: Node, selected: Node[]): boolean {
170183
return selected.includes(node)
171184
},
@@ -241,6 +254,10 @@ $height: 64px;
241254
242255
&__icon {
243256
color: var(--color-text-maxcontrast);
257+
258+
&.folder-icon {
259+
color: var(--color-primary-element);
260+
}
244261
}
245262
246263
&__preview {

lib/components/UploadPicker.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@
8181
import type { Entry, Node } from '@nextcloud/files'
8282
import type { PropType } from 'vue'
8383
84+
import { defineComponent } from 'vue'
8485
import { getNewFileMenuEntries, Folder } from '@nextcloud/files'
8586
import { showError } from '@nextcloud/dialogs'
8687
import makeEta from 'simple-eta'
87-
import Vue from 'vue'
8888
8989
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
9090
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
@@ -102,8 +102,9 @@ import { Status as UploadStatus } from '../upload.ts'
102102
import { t } from '../utils/l10n.ts'
103103
import logger from '../utils/logger.ts'
104104
105-
export default Vue.extend({
105+
export default defineComponent({
106106
name: 'UploadPicker',
107+
107108
components: {
108109
Cancel,
109110
NcActionButton,
@@ -318,7 +319,6 @@ export default Vue.extend({
318319
return
319320
}
320321
321-
logger.debug('Destination set', { destination })
322322
this.uploadManager.destination = destination
323323
324324
// If the destination change, we need to refresh the menu

lib/index.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Node } from '@nextcloud/files'
2+
import type { AsyncComponent } from 'vue'
23

34
import { Uploader } from './uploader'
45
import UploadPicker from './components/UploadPicker.vue'
6+
import Vue, { defineAsyncComponent } from 'vue'
57

68
export type { Uploader } from './uploader'
79
export { Status as UploaderStatus } from './uploader'
@@ -53,31 +55,35 @@ export function upload(destinationPath: string, file: File): Uploader {
5355
* @return {Promise<ConflictResolutionResult>} the selected and renamed files
5456
*/
5557
export async function openConflictPicker(dirname: string, conflicts: (File|Node)[], content: Node[]): Promise<ConflictResolutionResult> {
56-
const { default: ConflictPicker } = await import('./components/ConflictPicker.vue')
58+
const ConflictPicker = defineAsyncComponent(() => import('./components/ConflictPicker.vue')) as AsyncComponent
5759
return new Promise((resolve, reject) => {
58-
const picker = new ConflictPicker({
59-
propsData: {
60-
dirname,
61-
conflicts,
62-
content,
63-
},
64-
})
65-
66-
// Add listeners
67-
picker.$on('submit', (results: ConflictResolutionResult) => {
68-
// Return the results
69-
resolve(results)
70-
71-
// Destroy the component
72-
picker.$destroy()
73-
picker.$el?.parentNode?.removeChild(picker.$el)
74-
})
75-
picker.$on('cancel', (error?: Error) => {
76-
reject(error ?? new Error('Canceled'))
77-
78-
// Destroy the component
79-
picker.$destroy()
80-
picker.$el?.parentNode?.removeChild(picker.$el)
60+
const picker = new Vue({
61+
name: 'ConflictPickerRoot',
62+
render: (h) => h(ConflictPicker, {
63+
props: {
64+
dirname,
65+
conflicts,
66+
content,
67+
},
68+
on: {
69+
submit(results: ConflictResolutionResult) {
70+
// Return the results
71+
resolve(results)
72+
73+
// Destroy the component
74+
picker.$destroy()
75+
picker.$el?.parentNode?.removeChild(picker.$el)
76+
},
77+
cancel(error?: Error) {
78+
// Reject the promise
79+
reject(error ?? new Error('Canceled'))
80+
81+
// Destroy the component
82+
picker.$destroy()
83+
picker.$el?.parentNode?.removeChild(picker.$el)
84+
},
85+
},
86+
}),
8187
})
8288

8389
// Mount the component

lib/uploader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export class Uploader {
8585
if (!folder) {
8686
throw new Error('Invalid destination folder')
8787
}
88+
89+
logger.debug('Destination set', { folder })
8890
this._destinationFolder = folder
8991
}
9092

0 commit comments

Comments
 (0)