Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
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: 2 additions & 4 deletions examples/transloadit/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ImageEditor from '@uppy/image-editor'
import RemoteSources from '@uppy/remote-sources'
import Transloadit, { COMPANION_URL } from '@uppy/transloadit'
import Webcam from '@uppy/webcam'
import GoldenRetriever from '@uppy/golden-retriever'

import '@uppy/core/css/style.css'
import '@uppy/dashboard/css/style.css'
Expand Down Expand Up @@ -119,9 +120,6 @@ window.formUppyWithDashboard = formUppyWithDashboard
const dashboard = new Uppy({
debug: true,
autoProceed: false,
restrictions: {
allowedFileTypes: ['.png'],
},
})
.use(Dashboard, {
inline: true,
Expand All @@ -139,7 +137,7 @@ const dashboard = new Uppy({
template_id: TEMPLATE_ID,
},
},
})
}).use(GoldenRetriever)

window.dashboard = dashboard

Expand Down
1 change: 1 addition & 0 deletions examples/transloadit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@uppy/remote-sources": "workspace:*",
"@uppy/transloadit": "workspace:*",
"@uppy/webcam": "workspace:*",
"@uppy/golden-retriever": "workspace:*",
"express": "^4.22.0",
"he": "^1.2.0"
},
Expand Down
23 changes: 23 additions & 0 deletions packages/@uppy/core/src/Uppy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1854,6 +1854,29 @@ export class Uppy<
},
},
})

// Drop any currentUpload whose fileIDs are now all complete. This
// mirrors what `#removeUpload` does at the end of `#runUpload`, but
// is needed for completion paths that bypass `#runUpload` — notably
// SSE-driven Transloadit assembly completion during GoldenRetriever
// restore (no Resume click → no `uppy.restore(uploadID)` → orphaned
// entry in `currentUploads`, which then makes `uppy.clear()` throw
// when an uploader has `individualCancellation: false`).
const { currentUploads, files } = this.getState()
const updatedCurrentUploads = Object.fromEntries(
Object.entries(currentUploads).filter(([, upload]) =>
upload.fileIDs.some((id) => {
const f = files[id]
return f && !f.progress.complete && !f.error
Comment on lines +1867 to +1870

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer pruning currentUploads until upload finalization

Pruning currentUploads in the postprocess-complete event can remove an upload that is still inside #runUpload(). In Transloadit wait flows, postprocess-complete is emitted as soon as assembly completion is observed, before the postprocessor step fully returns, so this branch can delete currentUploads[uploadID] mid-run. After that, #runUpload() reads getCurrentUpload() as missing, exits early, and upload() can emit a complete payload with empty successful/failed arrays (and skip any remaining postprocessors).

Useful? React with 👍 / 👎.

}),
),
)
if (
Object.keys(updatedCurrentUploads).length !==
Object.keys(currentUploads).length
) {
this.setState({ currentUploads: updatedCurrentUploads })
}
})

this.on('restored', () => {
Expand Down
55 changes: 53 additions & 2 deletions packages/@uppy/dashboard/src/components/StatusBar/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,25 @@ type StatusBarProps<M extends Meta, B extends Body> = {
i18n: I18n
}

function getUploadingState(
/**
* Mirror of Transloadit's `AssemblyStatus['ok']` union (see `@transloadit/types`).
* Kept structural here so `@uppy/dashboard` does not depend on `@uppy/transloadit`.
*/
export type AssemblyOk =
| 'ASSEMBLY_UPLOADING'
| 'ASSEMBLY_EXECUTING'
| 'ASSEMBLY_COMPLETED'
| 'ASSEMBLY_CANCELED'
| 'ASSEMBLY_EXPIRED'
| 'ASSEMBLY_REPLAYING'
| 'REQUEST_ABORTED'

export function getUploadingState(
error: unknown,
isAllComplete: boolean,
recoveredState: State<any, any>['recoveredState'],
files: Record<string, UppyFile<any, any>>,
assemblyOk?: AssemblyOk,
): StatusBarUIProps<any, any>['uploadState'] {
if (error) {
return statusBarStates.STATE_ERROR
Expand All @@ -35,6 +49,34 @@ function getUploadingState(
return statusBarStates.STATE_COMPLETE
}

// A live (non-terminal) Transloadit assembly outranks the "recovered, press
// Upload" prompt — but only when the user has no action to take. See #6017.
//
// ASSEMBLY_EXECUTING implies all uploads are done by Transloadit's state
// machine, so it always outranks recoveredState.
//
// ASSEMBLY_UPLOADING is ambiguous: server may be waiting for more bytes
// (tus partial / Companion mid-flight) OR may already have everything and
// be about to transition to EXECUTING. We can disambiguate by file progress:
// if every started file has uploadComplete:true, the bytes are on the server
// and the Resume button would be a no-op — show progress UI instead. If any
// file still has work to do, KEEP the Resume button so the user can trigger
// 'restore-confirmed' → uppy.restore(uploadId), which is the only path that
// re-invokes tus / Companion (Transloadit disables tus auto-resume via
// `storeFingerprintForResuming: false`).
if (assemblyOk === 'ASSEMBLY_EXECUTING') {
return statusBarStates.STATE_POSTPROCESSING
}
if (assemblyOk === 'ASSEMBLY_UPLOADING') {
const allUploadsComplete = Object.values(files).every(
(f) => f.progress.uploadStarted && f.progress.uploadComplete,
)
if (allUploadsComplete) {
return statusBarStates.STATE_UPLOADING
}
// Fall through: any unfinished upload means the user must press Resume.
}

if (recoveredState) {
return statusBarStates.STATE_WAITING
}
Expand Down Expand Up @@ -198,14 +240,22 @@ export default class StatusBar<
}

render(): ComponentChild {
const state = this.props.uppy.getState()
const {
capabilities,
files,
allowNewUpload,
totalProgress,
error,
recoveredState,
} = this.props.uppy.getState()
} = state
// Read Transloadit's live assembly status `.ok` (if installed) so a live
// assembly can outrank the recovery prompt — see issue #6017.
const assemblyOk = (
state.plugins?.Transloadit as
| { assemblyStatus?: { ok?: AssemblyOk } }
| undefined
)?.assemblyStatus?.ok

const {
newFiles,
Expand Down Expand Up @@ -256,6 +306,7 @@ export default class StatusBar<
isAllComplete,
recoveredState,
files || {},
assemblyOk,
)}
allowNewUpload={allowNewUpload}
totalProgress={totalProgress}
Expand Down
54 changes: 53 additions & 1 deletion packages/@uppy/status-bar/src/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,25 @@ declare module '@uppy/core' {
const speedFilterHalfLife = 2000
const ETAFilterHalfLife = 2000

function getUploadingState(
/**
* Mirror of Transloadit's `AssemblyStatus['ok']` union (see `@transloadit/types`).
* Kept structural here so `@uppy/status-bar` does not depend on `@uppy/transloadit`.
*/
type AssemblyOk =
| 'ASSEMBLY_UPLOADING'
| 'ASSEMBLY_EXECUTING'
| 'ASSEMBLY_COMPLETED'
| 'ASSEMBLY_CANCELED'
| 'ASSEMBLY_EXPIRED'
| 'ASSEMBLY_REPLAYING'
| 'REQUEST_ABORTED'

export function getUploadingState(
error: unknown,
isAllComplete: boolean,
recoveredState: unknown,
files: Record<string, UppyFile<any, any>>,
assemblyOk?: AssemblyOk,
): StatusBarUIProps<any, any>['uploadState'] {
if (error) {
return statusBarStates.STATE_ERROR
Expand All @@ -38,6 +52,34 @@ function getUploadingState(
return statusBarStates.STATE_COMPLETE
}

// A live (non-terminal) Transloadit assembly outranks the "recovered, press
// Upload" prompt — but only when the user has no action to take. See #6017.
//
// ASSEMBLY_EXECUTING implies all uploads are done by Transloadit's state
// machine, so it always outranks recoveredState.
//
// ASSEMBLY_UPLOADING is ambiguous: server may be waiting for more bytes
// (tus partial / Companion mid-flight) OR may already have everything and
// be about to transition to EXECUTING. We can disambiguate by file progress:
// if every started file has uploadComplete:true, the bytes are on the server
// and the Resume button would be a no-op — show progress UI instead. If any
// file still has work to do, KEEP the Resume button so the user can trigger
// 'restore-confirmed' → uppy.restore(uploadId), which is the only path that
// re-invokes tus / Companion (Transloadit disables tus auto-resume via
// `storeFingerprintForResuming: false`).
if (assemblyOk === 'ASSEMBLY_EXECUTING') {
return statusBarStates.STATE_POSTPROCESSING
}
if (assemblyOk === 'ASSEMBLY_UPLOADING') {
const allUploadsComplete = Object.values(files).every(
(f) => f.progress.uploadStarted && f.progress.uploadComplete,
)
if (allUploadsComplete) {
return statusBarStates.STATE_UPLOADING
}
// Fall through: any unfinished upload means the user must press Resume.
}

if (recoveredState) {
return statusBarStates.STATE_WAITING
}
Expand Down Expand Up @@ -217,13 +259,23 @@ export default class StatusBar<M extends Meta, B extends Body> extends UIPlugin<
total: totalSize,
})

// Transloadit's plugin state, if installed, exposes the live assembly
// status. Reading `.ok` here lets a non-terminal assembly outrank the
// "press Upload to resume" UI on restore — see issue #6017.
const assemblyOk = (
state.plugins?.Transloadit as
| { assemblyStatus?: { ok?: AssemblyOk } }
| undefined
)?.assemblyStatus?.ok

return StatusBarUI({
error,
uploadState: getUploadingState(
error,
isAllComplete,
recoveredState,
state.files || {},
assemblyOk,
),
allowNewUpload,
totalProgress,
Expand Down
7 changes: 7 additions & 0 deletions packages/@uppy/transloadit/src/Assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ class TransloaditAssembly extends Emitter {

this.#sse.addEventListener('assembly_execution_progress', (e) => {
const details = JSON.parse(e.data)
// setting combined execution progress of the assembly
if (typeof details.progress_combined === 'number') {
this.status = {
...this.status,
execution_progress: details.progress_combined,
}
}
this.emit('execution-progress', details)
})

Expand Down
Loading
Loading