Skip to content

Commit e29c6d3

Browse files
committed
feat: add assembling status to UploadPicker
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent 6184c17 commit e29c6d3

File tree

6 files changed

+192
-22
lines changed

6 files changed

+192
-22
lines changed

cypress/components/UploadPicker/UploadPicker.cy.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,6 @@ describe('UploadPicker valid uploads', () => {
138138
afterEach(() => resetDocument())
139139

140140
it('Uploads a file with chunking', () => {
141-
// Init and reset chunk request spy
142-
const chunksRequestsSpy = cy.spy()
143-
144141
// Intercept tmp upload chunks folder creation
145142
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
146143
statusCode: 201,
@@ -151,7 +148,6 @@ describe('UploadPicker valid uploads', () => {
151148
method: 'PUT',
152149
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
153150
}, (req) => {
154-
chunksRequestsSpy()
155151
req.reply({
156152
statusCode: 201,
157153
})
@@ -193,7 +189,7 @@ describe('UploadPicker valid uploads', () => {
193189
cy.get('[data-cy-upload-picker] .upload-picker__progress')
194190
.as('progress')
195191
.should('not.be.visible')
196-
expect(chunksRequestsSpy).to.have.always.been.callCount(26)
192+
cy.get('@chunks.all').should('have.lengthOf', 26)
197193
})
198194
})
199195

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/* eslint-disable no-unused-expressions */
2+
/**
3+
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*/
6+
// dist file might not be built when running eslint only
7+
// eslint-disable-next-line import/no-unresolved,n/no-missing-import
8+
import { Folder, Permission } from '@nextcloud/files'
9+
import { generateRemoteUrl } from '@nextcloud/router'
10+
import { getUploader, UploadPicker } from '../../../lib/index.ts'
11+
12+
let state: string | undefined
13+
before(() => {
14+
cy.window().then((win) => {
15+
state = win.document.body.innerHTML
16+
})
17+
})
18+
19+
const resetDocument = () => {
20+
if (state) {
21+
cy.window().then((win) => {
22+
win.document.body.innerHTML = state!
23+
})
24+
}
25+
}
26+
27+
describe('UploadPicker: status testing', () => {
28+
beforeEach(() => {
29+
// Make sure we reset the destination
30+
// so other tests do not interfere
31+
const propsData = {
32+
destination: new Folder({
33+
id: 56,
34+
owner: 'user',
35+
source: generateRemoteUrl('dav/files/user'),
36+
permissions: Permission.ALL,
37+
root: '/files/user',
38+
}),
39+
}
40+
41+
// Mount picker
42+
const onPause = cy.spy().as('pausedListener')
43+
const onResume = cy.spy().as('resumedListener')
44+
cy.mount(UploadPicker, {
45+
propsData,
46+
listeners: {
47+
paused: onPause,
48+
resumed: onResume,
49+
},
50+
}).as('uploadPicker')
51+
52+
// Check and init aliases
53+
cy.get('[data-cy-upload-picker] [data-cy-upload-picker-input]').as('input').should('exist')
54+
cy.get('[data-cy-upload-picker] .upload-picker__progress').as('progress').should('exist')
55+
})
56+
57+
afterEach(() => resetDocument())
58+
59+
it('shows paused status on pause', () => {
60+
// Intercept tmp upload chunks folder creation
61+
cy.intercept('MKCOL', '/remote.php/dav/uploads/*/web-file-upload*', {
62+
statusCode: 201,
63+
}).as('init')
64+
65+
// Intercept chunks upload
66+
cy.intercept({
67+
method: 'PUT',
68+
url: '/remote.php/dav/uploads/*/web-file-upload*/*',
69+
}, (req) => {
70+
req.reply({
71+
statusCode: 201,
72+
})
73+
}).as('chunks')
74+
75+
// Intercept final assembly request
76+
const assemblyStartStub = cy.stub().as('assemblyStart')
77+
cy.intercept('MOVE', '/remote.php/dav/uploads/*/web-file-upload*/.file', (req) => {
78+
assemblyStartStub()
79+
req.reply({
80+
statusCode: 204,
81+
// Fake assembling chunks
82+
delay: 5000,
83+
})
84+
}).as('assemblyEnd')
85+
86+
// Start upload
87+
cy.get('@input').attachFile({
88+
// Fake file of 256MB
89+
fileContent: new Blob([new ArrayBuffer(256 * 1024 * 1024)]),
90+
fileName: 'photos.zip',
91+
mimeType: 'application/zip',
92+
encoding: 'utf8',
93+
lastModified: new Date().getTime(),
94+
})
95+
96+
cy.wait('@init').then(() => {
97+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
98+
.as('progress')
99+
.should('be.visible')
100+
})
101+
102+
cy.wait('@chunks').then(() => {
103+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
104+
.as('progress')
105+
.should('be.visible')
106+
cy.get('@progress')
107+
.children('progress')
108+
.should('not.have.value', '0')
109+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'estimating time left')
110+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
111+
112+
cy.wait(1000).then(() => {
113+
getUploader().pause()
114+
})
115+
116+
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'paused')
117+
cy.get('@pausedListener').should('have.been.calledOnce')
118+
119+
cy.wait(1000).then(() => {
120+
getUploader().start()
121+
})
122+
123+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
124+
cy.get('@resumedListener').should('have.been.calledOnce')
125+
})
126+
127+
// Should will retry until success or timeout
128+
cy.get('@assemblyStart', { timeout: 30000 }).should('have.been.calledOnce').then(() => {
129+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
130+
.as('progress')
131+
.should('be.visible')
132+
133+
cy.get('[data-cy-upload-picker-progress-label]').should('not.contain', 'paused')
134+
cy.get('[data-cy-upload-picker-progress-label]').should('contain', 'assembling')
135+
})
136+
137+
cy.wait('@assemblyEnd', { timeout: 60000 }).then(() => {
138+
cy.get('[data-cy-upload-picker] .upload-picker__progress')
139+
.as('progress')
140+
.should('not.be.visible')
141+
})
142+
})
143+
})

cypress/support/component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { mount } from '@cypress/vue2'
2929

3030
// @ts-expect-error Mock window so this is an internal property
3131
window._oc_capabilities = { files: {} }
32+
// @ts-expect-error Mock window so this is an internal property
33+
window._oc_debug = true
3234

3335
// Example use:
3436
// cy.mount(MyComponent)

l10n/messages.pot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ msgstr ""
3131
msgid "a few seconds left"
3232
msgstr ""
3333

34+
msgid "assembling"
35+
msgstr ""
36+
3437
msgid "Cancel"
3538
msgstr ""
3639

lib/components/UploadPicker.vue

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@
105105
</NcActions>
106106

107107
<!-- Progressbar and status -->
108-
<div v-show="isUploading" class="upload-picker__progress">
108+
<div v-show="isUploading" class="upload-picker__progress" data-cy-upload-picker-progress>
109109
<NcProgressBar :aria-label="t('Upload progress')"
110110
:aria-describedby="progressTimeId"
111111
:error="hasFailure"
112112
:value="progress"
113113
size="medium" />
114-
<p :id="progressTimeId">
115-
{{ timeLeft }}
114+
<p :id="progressTimeId" data-cy-upload-picker-progress-label>
115+
{{ status }}
116116
</p>
117117
</div>
118118

@@ -250,7 +250,7 @@ export default defineComponent({
250250
return {
251251
eta: null as null|ReturnType<typeof makeEta>,
252252
openedMenu: false,
253-
timeLeft: '',
253+
status: '',
254254
255255
newFileMenuEntries: [] as Entry[],
256256
uploadManager: getUploader(),
@@ -294,16 +294,27 @@ export default defineComponent({
294294
},
295295
296296
hasFailure(): boolean {
297-
return this.queue?.filter((upload: Upload) => upload.status === UploadStatus.FAILED).length !== 0
297+
return this.queue?.some((upload: Upload) => upload.status === UploadStatus.FAILED)
298298
},
299299
isUploading(): boolean {
300300
return this.queue?.length > 0
301301
},
302-
isAssembling(): boolean {
303-
return this.queue?.filter((upload: Upload) => upload.status === UploadStatus.ASSEMBLING).length !== 0
302+
isOnlyAssembling(): boolean {
303+
return !this.queue?.some((upload: Upload) => {
304+
// ignore empty uploads or meta uploads
305+
if (upload.size === 0) {
306+
return false
307+
}
308+
// If all the uploads are assembling or finished, the ongoing task is assembling
309+
return upload.status !== UploadStatus.ASSEMBLING && upload.status !== UploadStatus.FINISHED
310+
})
304311
},
305312
isPaused(): boolean {
306-
return this.uploadManager.info?.status === Status.PAUSED
313+
return this.uploaderStatus === Status.PAUSED
314+
},
315+
316+
uploaderStatus(): Status {
317+
return this.uploadManager.info?.status || Status.IDLE
307318
},
308319
309320
buttonLabel(): string {
@@ -347,12 +358,17 @@ export default defineComponent({
347358
this.updateStatus()
348359
},
349360
350-
isPaused(isPaused) {
351-
if (isPaused) {
361+
uploaderStatus(status, oldStatus) {
362+
if (status === Status.PAUSED) {
352363
this.$emit('paused', this.queue)
353-
} else {
364+
} else if (oldStatus === Status.PAUSED) {
354365
this.$emit('resumed', this.queue)
355366
}
367+
this.updateStatus()
368+
},
369+
370+
isOnlyAssembling() {
371+
this.updateStatus()
356372
},
357373
},
358374
@@ -444,28 +460,33 @@ export default defineComponent({
444460
445461
updateStatus() {
446462
if (this.isPaused) {
447-
this.timeLeft = t('paused')
463+
this.status = t('paused')
464+
return
465+
}
466+
467+
if (this.isOnlyAssembling) {
468+
this.status = t('assembling')
448469
return
449470
}
450471
451-
const estimate = Math.round(this.eta!.estimate())
472+
const estimate = Math.round(this.eta?.estimate?.() || 0)
452473
453474
if (estimate === Infinity) {
454-
this.timeLeft = t('estimating time left')
475+
this.status = t('estimating time left')
455476
return
456477
}
457478
if (estimate < 10) {
458-
this.timeLeft = t('a few seconds left')
479+
this.status = t('a few seconds left')
459480
return
460481
}
461482
if (estimate > 60) {
462483
const date = new Date(0)
463484
date.setSeconds(estimate)
464485
const time = date.toISOString().slice(11, 11 + 8)
465-
this.timeLeft = t('{time} left', { time }) // TRANSLATORS time has the format 00:00:00
486+
this.status = t('{time} left', { time }) // TRANSLATORS time has the format 00:00:00
466487
return
467488
}
468-
this.timeLeft = t('{seconds} seconds left', { seconds: estimate })
489+
this.status = t('{seconds} seconds left', { seconds: estimate })
469490
},
470491
471492
setDestination(destination: Folder) {

lib/uploader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ export class Uploader {
174174
public pause() {
175175
this._jobQueue.pause()
176176
this._queueStatus = Status.PAUSED
177+
this.updateStats()
178+
logger.debug('Upload paused')
177179
}
178180

179181
/**
@@ -183,6 +185,7 @@ export class Uploader {
183185
this._jobQueue.start()
184186
this._queueStatus = Status.UPLOADING
185187
this.updateStats()
188+
logger.debug('Upload resumed')
186189
}
187190

188191
/**
@@ -512,6 +515,8 @@ export class Uploader {
512515
await Promise.all(chunksQueue)
513516
this.updateStats()
514517

518+
// Assemble the chunks
519+
upload.status = UploadStatus.ASSEMBLING
515520
upload.response = await axios.request({
516521
method: 'MOVE',
517522
url: `${tempUrl}/.file`,

0 commit comments

Comments
 (0)