Skip to content

Commit 82bf9f9

Browse files
authored
Merge pull request #1123 from nextcloud-libraries/fix/conflict-picker
2 parents be1c52a + 426015e commit 82bf9f9

File tree

11 files changed

+913
-806
lines changed

11 files changed

+913
-806
lines changed

cypress/components/ConflictPicker.cy.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('ConflictPicker rendering', { testIsolation: true }, () => {
4747
describe('ConflictPicker resolving', () => {
4848
let images: File[] = []
4949

50-
before(() => {
50+
beforeEach(() => {
5151
images = []
5252
cy.fixture('image.jpg', null).then((content) => {
5353
images.push(new File([content], 'image1.jpg', { type: 'image/jpeg' }))
@@ -84,7 +84,7 @@ describe('ConflictPicker resolving', () => {
8484
listeners: {
8585
submit: onSubmit,
8686
cancel: onCancel,
87-
}
87+
},
8888
})
8989

9090
cy.get('[data-cy-conflict-picker-form]').should('be.visible')
@@ -129,7 +129,7 @@ describe('ConflictPicker resolving', () => {
129129
listeners: {
130130
submit: onSubmit,
131131
cancel: onCancel,
132-
}
132+
},
133133
})
134134

135135
cy.get('[data-cy-conflict-picker-form]').should('be.visible')
@@ -174,7 +174,7 @@ describe('ConflictPicker resolving', () => {
174174
listeners: {
175175
submit: onSubmit,
176176
cancel: onCancel,
177-
}
177+
},
178178
})
179179

180180
cy.get('[data-cy-conflict-picker-form]').should('be.visible')
@@ -221,7 +221,7 @@ describe('ConflictPicker resolving', () => {
221221
listeners: {
222222
submit: onSubmit,
223223
cancel: onCancel,
224-
}
224+
},
225225
})
226226

227227
cy.get('[data-cy-conflict-picker-form]').should('be.visible')
@@ -269,7 +269,7 @@ describe('ConflictPicker resolving', () => {
269269
listeners: {
270270
submit: onSubmit,
271271
cancel: onCancel,
272-
}
272+
},
273273
})
274274

275275
cy.get('[data-cy-conflict-picker-form]').should('be.visible')

cypress/support/component.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,31 @@ declare global {
3636
Cypress.Commands.add('mount', (component, optionsOrProps) => {
3737
let instance = null
3838

39-
// Add our mounted method to exposethe component instance to cypress
40-
if (!component?.options?.mounted) {
41-
component.options.mounted = []
39+
// Add our mounted method to expose the component instance to cypress
40+
41+
// Support old vue 2 options
42+
if (component.options) {
43+
if (!component.options.mounted) {
44+
component.options.mounted = []
45+
}
46+
component.options.mounted.push(function() {
47+
instance = this
48+
})
4249
}
4350

44-
component.options.mounted.push(function() {
45-
instance = this
46-
})
51+
// Support new vue 3 options
52+
if (typeof component?.mounted !== 'function') {
53+
component.mounted = function() {
54+
instance = this
55+
}
56+
} else {
57+
// If the component already has a mounted method, we need to chain them
58+
const originalMounted = component.mounted
59+
component.mounted = function() {
60+
originalMounted.call(this)
61+
instance = this
62+
}
63+
}
4764

4865
// Expose the component with cy.get('@component')
4966
return mount(component, optionsOrProps).then(() => {

l10n/messages.pot

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ msgstr ""
2222
msgid "a few seconds left"
2323
msgstr ""
2424

25+
msgid "Cancel"
26+
msgstr ""
27+
28+
msgid "Cancel the entire operation"
29+
msgstr ""
30+
2531
msgid "Cancel uploads"
2632
msgstr ""
2733

@@ -78,6 +84,9 @@ msgstr ""
7884
msgid "Upload progress"
7985
msgstr ""
8086

87+
msgid "When an incoming folder is selected, any conflicting files within it will also be overwritten."
88+
msgstr ""
89+
8190
msgid "Which files do you want to keep?"
8291
msgstr ""
8392

lib/components/ConflictPicker.vue

Lines changed: 66 additions & 21 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
})
@@ -283,7 +310,7 @@ export default Vue.extend({
283310
const selected = this.newSelected.filter((node: File|Node) => {
284311
const name = (node instanceof File) ? node.name : node.basename
285312
// files that are not in the old selection
286-
return !selectedOldNames.includes(name)
313+
return !selectedOldNames.includes(name) && !toRename.includes(node)
287314
}) as (File|Node)[]
288315
289316
logger.debug('Conflict resolved', { selected, renamed })
@@ -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>

0 commit comments

Comments
 (0)