mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 10:25:33 +00:00
Merge branch 'main' into ai-image-generator
This commit is contained in:
commit
7be8eaaa28
65 changed files with 1647 additions and 200 deletions
5
.changeset/cute-clocks-crash.md
Normal file
5
.changeset/cute-clocks-crash.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/locales": patch
|
||||
---
|
||||
|
||||
Improve zh-CN and zh-TW locale
|
||||
5
.changeset/every-wings-behave.md
Normal file
5
.changeset/every-wings-behave.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/locales": patch
|
||||
---
|
||||
|
||||
Improve Dutch locale
|
||||
36
.changeset/nasty-friends-win.md
Normal file
36
.changeset/nasty-friends-win.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
"@uppy/google-photos-picker": minor
|
||||
"@uppy/google-drive-picker": minor
|
||||
"@uppy/thumbnail-generator": minor
|
||||
"@uppy/golden-retriever": minor
|
||||
"@uppy/provider-views": minor
|
||||
"@uppy/remote-sources": minor
|
||||
"@uppy/screen-capture": minor
|
||||
"@uppy/google-drive": minor
|
||||
"@uppy/image-editor": minor
|
||||
"@uppy/drop-target": minor
|
||||
"@uppy/transloadit": minor
|
||||
"@uppy/compressor": minor
|
||||
"@uppy/status-bar": minor
|
||||
"@uppy/xhr-upload": minor
|
||||
"@uppy/dashboard": minor
|
||||
"@uppy/drag-drop": minor
|
||||
"@uppy/instagram": minor
|
||||
"@uppy/facebook": minor
|
||||
"@uppy/onedrive": minor
|
||||
"@uppy/unsplash": minor
|
||||
"@uppy/dropbox": minor
|
||||
"@uppy/aws-s3": minor
|
||||
"@uppy/webcam": minor
|
||||
"@uppy/webdav": minor
|
||||
"@uppy/audio": minor
|
||||
"@uppy/core": minor
|
||||
"@uppy/form": minor
|
||||
"@uppy/zoom": minor
|
||||
"@uppy/box": minor
|
||||
"@uppy/tus": minor
|
||||
"@uppy/url": minor
|
||||
---
|
||||
|
||||
- Add PluginTypeRegistry and typed getPlugin overload in @uppy/core
|
||||
- Register plugin ids across packages so uppy.getPlugin('Dashboard' | 'Webcam') returns the concrete plugin type and removes the need to pass generics in getPlugin()
|
||||
9
.changeset/polite-cougars-itch.md
Normal file
9
.changeset/polite-cougars-itch.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
"@uppy/aws-s3": patch
|
||||
"@uppy/core": patch
|
||||
"@uppy/tus": patch
|
||||
"@uppy/utils": patch
|
||||
"@uppy/xhr-upload": patch
|
||||
---
|
||||
|
||||
Fix: Move completed uploads exclusion logic into uploaders. This fixes the problem where postprocessors would not run for already uploaded files.
|
||||
2
.github/workflows/bundlers.yml
vendored
2
.github/workflows/bundlers.yml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
|
|
|
|||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
node-version: [lts/*]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
|
|
@ -94,7 +94,7 @@ jobs:
|
|||
SKIP_YARN_COREPACK_CHECK: true
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
|
|
|||
10
.github/workflows/companion-deploy.yml
vendored
10
.github/workflows/companion-deploy.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Set SHA commit in version
|
||||
run:
|
||||
(cd packages/@uppy/companion && node -e 'const
|
||||
|
|
@ -46,16 +46,16 @@ jobs:
|
|||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: transloadit/companion
|
||||
tags: |
|
||||
type=edge
|
||||
type=raw,value=latest,enable=false
|
||||
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
|
|
@ -77,7 +77,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Alter dockerfile
|
||||
run: |
|
||||
sed -i 's/^EXPOSE 3020$/EXPOSE $PORT/g' Dockerfile
|
||||
|
|
|
|||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
diff: ${{ steps.diff.outputs.OUTPUT_DIFF }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
ref: ${{ github.event.pull_request && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
|
||||
|
|
|
|||
2
.github/workflows/lockfile_check.yml
vendored
2
.github/workflows/lockfile_check.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
|
|
|
|||
2
.github/workflows/manual-cdn.yml
vendored
2
.github/workflows/manual-cdn.yml
vendored
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
|
|
|
|||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
published: ${{ steps.changesets.outputs.published }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
|
@ -90,10 +90,10 @@ jobs:
|
|||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: transloadit/companion
|
||||
tags: |
|
||||
|
|
@ -101,7 +101,7 @@ jobs:
|
|||
type=semver,pattern={{version}},value=${{ needs.release.outputs.companionWasReleased }}
|
||||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable=true
|
||||
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import PermissionsScreen from './PermissionsScreen.js'
|
|||
import RecordingScreen from './RecordingScreen.js'
|
||||
import supportsMediaRecorder from './supportsMediaRecorder.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Audio: Audio<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export interface AudioOptions extends UIPluginOptions {
|
||||
showAudioSourceDropdown?: boolean
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from 'vitest'
|
||||
|
||||
import 'whatwg-fetch'
|
||||
import Core, { type UppyFile } from '@uppy/core'
|
||||
import Core, { type Meta, type UppyFile } from '@uppy/core'
|
||||
import nock from 'nock'
|
||||
import AwsS3Multipart, {
|
||||
type AwsBody,
|
||||
|
|
@ -36,8 +36,8 @@ describe('AwsS3Multipart', () => {
|
|||
let opts: Partial<AwsS3MultipartOptions<any, any>>
|
||||
|
||||
beforeEach(() => {
|
||||
const core = new Core<any, AwsBody>().use(AwsS3Multipart)
|
||||
const awsS3Multipart = core.getPlugin('AwsS3Multipart') as any
|
||||
const core = new Core<Meta, AwsBody>().use(AwsS3Multipart)
|
||||
const awsS3Multipart = core.getPlugin('AwsS3Multipart')!
|
||||
opts = awsS3Multipart.opts
|
||||
})
|
||||
|
||||
|
|
@ -125,8 +125,8 @@ describe('AwsS3Multipart', () => {
|
|||
const awsS3Multipart = core.getPlugin('AwsS3Multipart')!
|
||||
|
||||
const err = 'Expected a `endpoint` option'
|
||||
const file = {}
|
||||
const opts = {}
|
||||
const file = {} as unknown as UppyFile<Meta, Record<string, never>>
|
||||
const opts = {} as any
|
||||
|
||||
expect(() => awsS3Multipart.opts.createMultipartUpload(file)).toThrow(err)
|
||||
expect(() => awsS3Multipart.opts.listParts(file, opts)).toThrow(err)
|
||||
|
|
@ -202,11 +202,11 @@ describe('AwsS3Multipart', () => {
|
|||
})
|
||||
|
||||
describe('without companionUrl (custom main functions)', () => {
|
||||
let core: Core<any, AwsBody>
|
||||
let awsS3Multipart: AwsS3Multipart<any, AwsBody>
|
||||
let core: Core<Meta, AwsBody>
|
||||
let awsS3Multipart: AwsS3Multipart<Meta, AwsBody>
|
||||
|
||||
beforeEach(() => {
|
||||
core = new Core<any, AwsBody>()
|
||||
core = new Core<Meta, AwsBody>()
|
||||
core.use(AwsS3Multipart, {
|
||||
shouldUseMultipart: true,
|
||||
limit: 0,
|
||||
|
|
@ -226,7 +226,7 @@ describe('AwsS3Multipart', () => {
|
|||
}),
|
||||
listParts: undefined as any,
|
||||
})
|
||||
awsS3Multipart = core.getPlugin('AwsS3Multipart') as any
|
||||
awsS3Multipart = core.getPlugin('AwsS3Multipart')!
|
||||
})
|
||||
|
||||
it('Keeps chunks marked as busy through retries until they complete', async () => {
|
||||
|
|
@ -366,7 +366,6 @@ describe('AwsS3Multipart', () => {
|
|||
),
|
||||
listParts: undefined as any,
|
||||
})
|
||||
const awsS3Multipart = core.getPlugin('AwsS3Multipart')!
|
||||
const fileSize = 5 * MB + 1 * MB
|
||||
|
||||
core.addFile({
|
||||
|
|
@ -380,7 +379,7 @@ describe('AwsS3Multipart', () => {
|
|||
|
||||
await core.upload()
|
||||
|
||||
expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(3)
|
||||
expect(uploadPartBytes.mock.calls.length).toEqual(3)
|
||||
})
|
||||
|
||||
it('calls `upload-error` when uploadPartBytes fails after all retries', async () => {
|
||||
|
|
@ -406,7 +405,6 @@ describe('AwsS3Multipart', () => {
|
|||
listParts: undefined as any,
|
||||
})
|
||||
const fileSize = 5 * MB + 1 * MB
|
||||
const awsS3Multipart = core.getPlugin('AwsS3Multipart')!
|
||||
const uploadErrorMock = vi.fn()
|
||||
const uploadSuccessMock = vi.fn()
|
||||
core.on('upload-error', uploadErrorMock)
|
||||
|
|
@ -438,7 +436,7 @@ describe('AwsS3Multipart', () => {
|
|||
// Catch Promise.all reject
|
||||
}
|
||||
|
||||
expect(awsS3Multipart.opts.uploadPartBytes.mock.calls.length).toEqual(5)
|
||||
expect(uploadPartBytes.mock.calls.length).toEqual(5)
|
||||
expect(uploadErrorMock.mock.calls.length).toEqual(1)
|
||||
expect(uploadSuccessMock.mock.calls.length).toEqual(1) // This fails for me becuase upload returned early.
|
||||
})
|
||||
|
|
@ -526,8 +524,8 @@ describe('AwsS3Multipart', () => {
|
|||
})
|
||||
|
||||
describe('dynamic companionHeader using setOption', () => {
|
||||
let core: Core<any, AwsBody>
|
||||
let awsS3Multipart: AwsS3Multipart<any, AwsBody>
|
||||
let core: Core<Meta, AwsBody>
|
||||
let awsS3Multipart: AwsS3Multipart<Meta, AwsBody>
|
||||
const newToken = 'new token'
|
||||
|
||||
it('companionHeader is updated before uploading file', async () => {
|
||||
|
|
@ -536,7 +534,7 @@ describe('AwsS3Multipart', () => {
|
|||
/* Set up preprocessor */
|
||||
core.addPreProcessor(() => {
|
||||
awsS3Multipart =
|
||||
core.getPlugin<AwsS3Multipart<any, AwsBody>>('AwsS3Multipart')!
|
||||
core.getPlugin<AwsS3Multipart<Meta, AwsBody>>('AwsS3Multipart')!
|
||||
awsS3Multipart.setOptions({
|
||||
endpoint: 'http://localhost',
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import type {
|
|||
import {
|
||||
createAbortError,
|
||||
filterFilesToEmitUploadStarted,
|
||||
filterNonFailedFiles,
|
||||
filterFilesToUpload,
|
||||
getAllowedMetaFields,
|
||||
RateLimitedQueue,
|
||||
} from '@uppy/utils'
|
||||
|
|
@ -45,6 +45,9 @@ declare module '@uppy/core' {
|
|||
export interface UppyEventMap<M extends Meta, B extends Body> {
|
||||
's3-multipart:part-uploaded': PartUploadedCallback<M, B>
|
||||
}
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
AwsS3Multipart: AwsS3Multipart<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
function assertServerError<T>(res: T): T {
|
||||
|
|
@ -935,7 +938,7 @@ export default class AwsS3Multipart<
|
|||
if (fileIDs.length === 0) return undefined
|
||||
|
||||
const files = this.uppy.getFilesByIds(fileIDs)
|
||||
const filesFiltered = filterNonFailedFiles(files)
|
||||
const filesFiltered = filterFilesToUpload(files)
|
||||
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
|
||||
|
||||
this.uppy.emit('upload-start', filesToEmit)
|
||||
|
|
|
|||
|
|
@ -109,3 +109,9 @@ export default class Box<M extends Meta, B extends Body>
|
|||
return this.view.render(state)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Box: Box<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ declare module '@uppy/core' {
|
|||
export interface UppyEventMap<M extends Meta, B extends Body> {
|
||||
'compressor:complete': (file: UppyFile<M, B>[]) => void
|
||||
}
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Compressor: Compressor<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export interface CompressorOpts extends PluginOpts, CompressorJS.Options {
|
||||
|
|
|
|||
|
|
@ -2609,7 +2609,7 @@ describe('src/Core', () => {
|
|||
expect(infoVisibleEvent.mock.calls.length).toEqual(1)
|
||||
})
|
||||
|
||||
it('should set an info message to be displayed for a period of time before hiding', (done) => {
|
||||
it('should set an info message to be displayed for a period of time before hiding', async () => {
|
||||
const infoVisibleEvent = vi.fn()
|
||||
const infoHiddenEvent = vi.fn()
|
||||
const core = new Core()
|
||||
|
|
@ -2618,12 +2618,10 @@ describe('src/Core', () => {
|
|||
|
||||
core.info('This is the message', 'info', 100)
|
||||
expect(infoHiddenEvent.mock.calls.length).toEqual(0)
|
||||
setTimeout(() => {
|
||||
expect(infoHiddenEvent.mock.calls.length).toEqual(1)
|
||||
expect(core.getState().info).toEqual([])
|
||||
// @ts-ignore
|
||||
done()
|
||||
}, 110)
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 110))
|
||||
expect(infoHiddenEvent.mock.calls.length).toEqual(1)
|
||||
expect(core.getState().info).toEqual([])
|
||||
})
|
||||
|
||||
it('should hide an info message', () => {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,10 @@ export type UnknownProviderPluginState = {
|
|||
searchResults?: string[] | undefined
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyInterface: PluginTypeRegistry is extended via module augmentation
|
||||
// biome-ignore lint/correctness/noUnusedVariables: Type parameters are used in module augmentation
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {}
|
||||
|
||||
export interface AsyncStore {
|
||||
getItem: (key: string) => Promise<string | null>
|
||||
setItem: (key: string, value: string) => Promise<void>
|
||||
|
|
@ -1941,12 +1945,21 @@ export class Uppy<
|
|||
/**
|
||||
* Find one Plugin by name.
|
||||
*/
|
||||
|
||||
getPlugin<K extends keyof PluginTypeRegistry<M, B>>(
|
||||
id: K,
|
||||
): PluginTypeRegistry<M, B>[K] | undefined
|
||||
|
||||
getPlugin<T extends UnknownPlugin<M, B> = UnknownPlugin<M, B>>(
|
||||
id: string,
|
||||
): T | undefined {
|
||||
): T | undefined
|
||||
|
||||
getPlugin(id: string): UnknownPlugin<M, B> | undefined {
|
||||
for (const plugins of Object.values(this.#plugins)) {
|
||||
const foundPlugin = plugins.find((plugin) => plugin.id === id)
|
||||
if (foundPlugin != null) return foundPlugin as T
|
||||
if (foundPlugin != null) {
|
||||
return foundPlugin as UnknownPlugin<M, B>
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -2220,9 +2233,6 @@ export class Uppy<
|
|||
]
|
||||
try {
|
||||
for (let step = currentUpload.step || 0; step < steps.length; step++) {
|
||||
if (!currentUpload) {
|
||||
break
|
||||
}
|
||||
const fn = steps[step]
|
||||
|
||||
this.setState({
|
||||
|
|
@ -2235,13 +2245,7 @@ export class Uppy<
|
|||
},
|
||||
})
|
||||
|
||||
// when restoring (e.g. using golden retriever), we don't need to re-upload already successfully uploaded files
|
||||
// so let's exclude them here:
|
||||
// https://github.com/transloadit/uppy/issues/5930
|
||||
const fileIDs = currentUpload.fileIDs.filter((fileID) => {
|
||||
const file = this.getFile(fileID)
|
||||
return !file.progress.uploadComplete
|
||||
})
|
||||
const { fileIDs } = currentUpload
|
||||
|
||||
// TODO give this the `updatedUpload` object as its only parameter maybe?
|
||||
// Otherwise when more metadata may be added to the upload this would keep getting more parameters
|
||||
|
|
@ -2249,6 +2253,9 @@ export class Uppy<
|
|||
|
||||
// Update currentUpload value in case it was modified asynchronously.
|
||||
currentUpload = getCurrentUpload()
|
||||
if (!currentUpload) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.#removeUpload(uploadID)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export type {
|
|||
PartialTreeFolderNode,
|
||||
PartialTreeFolderRoot,
|
||||
PartialTreeId,
|
||||
PluginTypeRegistry,
|
||||
State,
|
||||
UnknownPlugin,
|
||||
UnknownProviderPlugin,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Body, InternalMetadata, LocaleStrings, Meta } from '@uppy/utils'
|
||||
import { expectTypeOf, test } from 'vitest'
|
||||
import BasePlugin from './BasePlugin.js'
|
||||
import UIPlugin, { type UIPluginOptions } from './UIPlugin.js'
|
||||
import Uppy, { type UnknownPlugin } from './Uppy.js'
|
||||
|
||||
|
|
@ -68,3 +69,72 @@ test('Meta and Body generic move through the Uppy class', async () => {
|
|||
|
||||
await core.upload()
|
||||
})
|
||||
|
||||
class TestRegistryPlugin<M extends Meta, B extends Body> extends BasePlugin<
|
||||
Record<string, never>,
|
||||
M,
|
||||
B
|
||||
> {
|
||||
constructor(uppy: Uppy<M, B>) {
|
||||
super(uppy, {})
|
||||
this.id = 'TestRegistryPlugin'
|
||||
this.type = 'acquirer'
|
||||
}
|
||||
}
|
||||
|
||||
// Augment the Type Registry with our test plugin
|
||||
declare module './Uppy.js' {
|
||||
interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
TestRegistryPlugin: TestRegistryPlugin<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
test('Type Registry: getPlugin with registered plugin name returns correct type', () => {
|
||||
const uppy = new Uppy()
|
||||
uppy.use(TestRegistryPlugin)
|
||||
|
||||
// When using a registered plugin name, TypeScript should infer the correct type from PluginTypeRegistry
|
||||
const plugin = uppy.getPlugin('TestRegistryPlugin')
|
||||
|
||||
expectTypeOf(plugin).toEqualTypeOf<
|
||||
TestRegistryPlugin<Meta, Record<string, never>> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
test('Type Registry: getPlugin with unregistered name returns UnknownPlugin', () => {
|
||||
const uppy = new Uppy()
|
||||
|
||||
// When using a non-registered string, should return UnknownPlugin
|
||||
const plugin = uppy.getPlugin('SomeRandomPlugin')
|
||||
|
||||
expectTypeOf(plugin).toEqualTypeOf<
|
||||
UnknownPlugin<Meta, Record<string, never>> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
test('Type Registry: getPlugin with dynamic string returns UnknownPlugin', () => {
|
||||
const uppy = new Uppy()
|
||||
const pluginName: string = 'DynamicName'
|
||||
|
||||
// Dynamic string should use the fallback overload unlike literal string
|
||||
const plugin = uppy.getPlugin(pluginName)
|
||||
|
||||
expectTypeOf(plugin).toEqualTypeOf<
|
||||
UnknownPlugin<Meta, Record<string, never>> | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
test('Type Registry: works with custom Meta and Body types', () => {
|
||||
type CustomMeta = { userId: string; timestamp: number }
|
||||
type CustomBody = { encrypted: boolean }
|
||||
|
||||
// With custom Meta and Body types
|
||||
const uppy = new Uppy<CustomMeta, CustomBody>()
|
||||
uppy.use(TestRegistryPlugin)
|
||||
|
||||
const plugin = uppy.getPlugin('TestRegistryPlugin')
|
||||
|
||||
expectTypeOf(plugin).toEqualTypeOf<
|
||||
TestRegistryPlugin<CustomMeta, CustomBody> | undefined
|
||||
>()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@uppy/core": "workspace:^",
|
||||
"@uppy/dropbox": "workspace:^",
|
||||
"@uppy/google-drive": "workspace:^",
|
||||
"@uppy/url": "workspace:^",
|
||||
"@uppy/webcam": "workspace:^",
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ declare module '@uppy/core' {
|
|||
'dashboard:file-edit-complete': DashboardFileEditCompleteCallback<M, B>
|
||||
'dashboard:close-panel': (id: string | undefined) => void
|
||||
}
|
||||
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Dashboard: Dashboard<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
interface PromiseWithResolvers<T> {
|
||||
|
|
|
|||
568
packages/@uppy/dashboard/src/GlobalSearch.browser.test.ts
Normal file
568
packages/@uppy/dashboard/src/GlobalSearch.browser.test.ts
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
import Uppy from '@uppy/core'
|
||||
import Dashboard from '@uppy/dashboard'
|
||||
import Dropbox from '@uppy/dropbox'
|
||||
import GoogleDrive from '@uppy/google-drive'
|
||||
import { ProviderViews } from '@uppy/provider-views'
|
||||
import { page, userEvent } from '@vitest/browser/context'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
|
||||
import { worker } from './setup.js'
|
||||
|
||||
import '@uppy/core/css/style.css'
|
||||
import '@uppy/dashboard/css/style.css'
|
||||
|
||||
let uppy: Uppy | undefined
|
||||
|
||||
/**
|
||||
* In Normal mode (ListItem.tsx), folders are rendered as buttons, whereas files are rendered as checkboxes with a corresponding <label>.
|
||||
* In Search mode (SearchListItem.tsx), both files and folders are rendered as buttons.
|
||||
* Because of this, in Normal mode, when checking whether a file exists, we need to use:
|
||||
* await expect.element(page.getByRole('button', { name:'nested-target.pdf', exact: true }))
|
||||
* whereas, in Search mode, we need to scope the query to the checkbox role instead when searching for a file
|
||||
*/
|
||||
|
||||
beforeAll(async () => {
|
||||
// Disable search debounce inside ProviderView during tests to avoid long sleeps
|
||||
// @ts-expect-error test-only hook
|
||||
ProviderViews[Symbol.for('uppy test: searchDebounceMs')] = 0
|
||||
await worker.start({
|
||||
onUnhandledRequest: 'bypass',
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await worker.stop()
|
||||
})
|
||||
|
||||
type SourceName = 'Dropbox' | 'GoogleDrive'
|
||||
|
||||
function initializeUppy(sources: SourceName[] = ['Dropbox']) {
|
||||
document.body.innerHTML = '<div id="app"></div>'
|
||||
|
||||
const instance = new Uppy({ id: 'uppy-e2e' }).use(Dashboard, {
|
||||
target: '#app',
|
||||
inline: true,
|
||||
height: 500,
|
||||
})
|
||||
|
||||
for (const source of sources) {
|
||||
if (source === 'Dropbox') {
|
||||
instance.use(Dropbox, { companionUrl: 'http://companion.test' })
|
||||
} else if (source === 'GoogleDrive') {
|
||||
instance.use(GoogleDrive, { companionUrl: 'http://companion.test' })
|
||||
}
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// Removed shared beforeEach initialization. Each test initializes its own Uppy instance.
|
||||
|
||||
afterEach(async () => {
|
||||
if (!uppy) return
|
||||
|
||||
// this is done to prevent the edgecase when all plugins are removed before dashboard is unmounted from UI
|
||||
// causing PickerPanelContent to crash
|
||||
const dashboard = uppy.getPlugin('Dashboard')
|
||||
dashboard?.hideAllPanels()
|
||||
const panelSelector = '[data-uppy-panelType="PickerPanel"]'
|
||||
if (document.querySelector(panelSelector)) {
|
||||
await expect.poll(() => document.querySelector(panelSelector)).toBeNull()
|
||||
}
|
||||
|
||||
uppy.destroy()
|
||||
uppy = undefined
|
||||
})
|
||||
|
||||
describe('ProviderView Search E2E', () => {
|
||||
test('Search for nested file in Dropbox and verify results', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
expect(searchInput).toBeDefined()
|
||||
|
||||
// Search mode (SearchResultItem.tsx): files and folders render as buttons;
|
||||
// use role=button for file assertions in search results.
|
||||
await userEvent.type(searchInput, 'target')
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'target.pdf', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const targetPdfItem = await page.getByRole('button', {
|
||||
name: 'target.pdf',
|
||||
exact: true,
|
||||
})
|
||||
expect(targetPdfItem).toBeTruthy()
|
||||
})
|
||||
|
||||
test('Search deep folder -> open it -> click ancestor breadcrumb and navigate correctly', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
|
||||
await userEvent.clear(searchInput)
|
||||
await userEvent.type(searchInput, 'second')
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'second' }))
|
||||
.toBeVisible()
|
||||
|
||||
const secondFolder = await page.getByRole('button', { name: 'second' })
|
||||
|
||||
await secondFolder.click()
|
||||
|
||||
// Normal mode (ListItem.tsx): files render as checkboxes with a corresponding <label>.
|
||||
// Use role=checkbox for file assertions in browse view.
|
||||
await expect
|
||||
.element(
|
||||
page.getByRole('checkbox', { name: 'deep-file.txt', exact: true }),
|
||||
)
|
||||
.toBeVisible()
|
||||
|
||||
// Click ancestor breadcrumb that was never loaded before in browse mode
|
||||
const firstBreadcrumb = page.getByRole('button', { name: 'first' })
|
||||
await firstBreadcrumb.click()
|
||||
|
||||
const hasSecondFolder = await page.getByRole('button', {
|
||||
name: 'second',
|
||||
exact: true,
|
||||
})
|
||||
expect(hasSecondFolder).toBeVisible()
|
||||
})
|
||||
|
||||
test('Check folder in browse mode, search for nested item -> nested item should be checked', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const firstFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find(
|
||||
(item) =>
|
||||
item.textContent?.includes('first') && item.querySelector('button'),
|
||||
)
|
||||
|
||||
const firstFolderCheckbox =
|
||||
firstFolderItem?.querySelector<HTMLInputElement>('input[type="checkbox"]')
|
||||
expect(firstFolderCheckbox).toBeTruthy()
|
||||
await firstFolderCheckbox!.click()
|
||||
|
||||
expect(firstFolderCheckbox!.checked).toBe(true)
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
await userEvent.type(searchInput, 'second')
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'second', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const secondFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find((item) => item.textContent?.includes('second'))
|
||||
const secondFolderCheckbox =
|
||||
secondFolderItem?.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)
|
||||
expect(secondFolderCheckbox).toBeTruthy()
|
||||
|
||||
// Children inherit checked state from parent
|
||||
expect(secondFolderCheckbox!.checked).toBe(true)
|
||||
})
|
||||
|
||||
test('Search for nested item, check it, go back to normal view -> parent should be partial', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
await userEvent.type(searchInput, 'second')
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'second', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const secondFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find((item) => item.textContent?.includes('second'))
|
||||
const secondFolderCheckbox =
|
||||
secondFolderItem?.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)
|
||||
expect(secondFolderCheckbox).toBeTruthy()
|
||||
await secondFolderCheckbox!.click()
|
||||
|
||||
expect(secondFolderCheckbox!.checked).toBe(true)
|
||||
|
||||
const clearSearchButton = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterReset',
|
||||
) as HTMLButtonElement
|
||||
expect(clearSearchButton).toBeDefined()
|
||||
await clearSearchButton.click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const firstFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find(
|
||||
(item) =>
|
||||
item.textContent?.includes('first') && item.querySelector('button'),
|
||||
)
|
||||
expect(firstFolderItem).toBeTruthy()
|
||||
|
||||
// Parent is partial when some (but not all) children are checked
|
||||
expect(
|
||||
firstFolderItem?.classList.contains(
|
||||
'uppy-ProviderBrowserItem--is-partial',
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
test('Search for nested item, check then uncheck it, go back to normal view -> parent should be unchecked', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
await userEvent.type(searchInput, 'second')
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'second', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const secondFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find((item) => item.textContent?.includes('second'))
|
||||
const secondFolderCheckbox =
|
||||
secondFolderItem?.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)
|
||||
expect(secondFolderCheckbox).toBeTruthy()
|
||||
|
||||
await secondFolderCheckbox!.click()
|
||||
expect(secondFolderCheckbox!.checked).toBe(true)
|
||||
|
||||
await secondFolderCheckbox!.click()
|
||||
expect(secondFolderCheckbox!.checked).toBe(false)
|
||||
|
||||
const clearSearchButton = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterReset',
|
||||
) as HTMLButtonElement
|
||||
expect(clearSearchButton).toBeDefined()
|
||||
await clearSearchButton.click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const firstFolderItem = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
).find(
|
||||
(item) =>
|
||||
item.textContent?.includes('first') && item.querySelector('button'),
|
||||
)
|
||||
expect(firstFolderItem).toBeTruthy()
|
||||
|
||||
expect(
|
||||
firstFolderItem?.classList.contains(
|
||||
'uppy-ProviderBrowserItem--is-checked',
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
firstFolderItem?.classList.contains(
|
||||
'uppy-ProviderBrowserItem--is-partial',
|
||||
),
|
||||
).toBe(false)
|
||||
|
||||
const firstFolderCheckbox =
|
||||
firstFolderItem?.querySelector<HTMLInputElement>('input[type="checkbox"]')
|
||||
expect(firstFolderCheckbox).toBeTruthy()
|
||||
expect(firstFolderCheckbox!.checked).toBe(false)
|
||||
})
|
||||
|
||||
test('Navigate into folder and perform scoped search -> should find nested files at multiple levels', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const firstFolderButton = page.getByRole('button', { name: 'first' })
|
||||
await firstFolderButton.click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'second' }))
|
||||
.toBeVisible()
|
||||
// Normal mode (ListItem.tsx): files render as checkboxes with corresponding <label>; scope by role=checkbox. refer to a commment at the top of the file for more detailed explanation.
|
||||
await expect
|
||||
.element(
|
||||
page.getByRole('checkbox', { name: 'intermediate.doc', exact: true }),
|
||||
)
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
expect(searchInput).toBeDefined()
|
||||
await userEvent.type(searchInput, 'target')
|
||||
// Search mode (SearchResultItem.tsx): files render as buttons; scope by role=button.
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'target.pdf', exact: true }))
|
||||
.toBeVisible()
|
||||
await expect
|
||||
.element(
|
||||
page.getByRole('button', { name: 'nested-target.pdf', exact: true }),
|
||||
)
|
||||
.toBeVisible()
|
||||
|
||||
const searchResults = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
|
||||
const targetFiles = searchResults.filter((item) =>
|
||||
item.textContent?.toLowerCase().includes('target'),
|
||||
)
|
||||
expect(targetFiles.length).toBe(2)
|
||||
})
|
||||
|
||||
test('No duplicate items when searching and then browsing to the same file', async () => {
|
||||
uppy = initializeUppy(['Dropbox'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Dropbox'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Dropbox' }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from dropbox/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
// Normal mode (ListItem.tsx): file is a checkbox; assert by role=checkbox. refer to a commment at the top of the file for more detailed explanation.
|
||||
await expect
|
||||
.element(page.getByRole('checkbox', { name: 'readme.md', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
await userEvent.type(searchInput, 'readme')
|
||||
// Search mode (SearchResultItem.tsx): file is a button; assert by role=button.
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'readme.md', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchResults = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
const readmeInSearch = searchResults.filter((item) =>
|
||||
item.textContent?.includes('readme.md'),
|
||||
)
|
||||
expect(readmeInSearch.length).toBe(1)
|
||||
|
||||
const clearSearchButton = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterReset',
|
||||
) as HTMLButtonElement
|
||||
expect(clearSearchButton).toBeDefined()
|
||||
await clearSearchButton.click()
|
||||
// proceed to verify browse results directly
|
||||
const browseResults = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
const readmeInBrowse = browseResults.filter((item) =>
|
||||
item.textContent?.includes('readme.md'),
|
||||
)
|
||||
expect(readmeInBrowse.length).toBe(1)
|
||||
|
||||
const readmeCheckbox = readmeInBrowse[0]?.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)
|
||||
expect(readmeCheckbox).toBeTruthy()
|
||||
await readmeCheckbox!.click()
|
||||
expect(readmeCheckbox!.checked).toBe(true)
|
||||
|
||||
// Verify checked state persists after searching again (same node in partialTree)
|
||||
await userEvent.clear(searchInput)
|
||||
await userEvent.type(searchInput, 'readme')
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'readme.md', exact: true }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchResultsAgain = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
const readmeInSearchAgain = searchResultsAgain.find((item) =>
|
||||
item.textContent?.includes('readme.md'),
|
||||
)
|
||||
const readmeCheckboxInSearch =
|
||||
readmeInSearchAgain?.querySelector<HTMLInputElement>(
|
||||
'input[type="checkbox"]',
|
||||
)
|
||||
expect(readmeCheckboxInSearch).toBeTruthy()
|
||||
expect(readmeCheckboxInSearch!.checked).toBe(true)
|
||||
})
|
||||
|
||||
test('Client-side filtering works for providers without server-side search (Google Drive)', async () => {
|
||||
uppy = initializeUppy(['GoogleDrive'])
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('presentation').getByText('Google Drive'))
|
||||
.toBeVisible()
|
||||
await page.getByRole('tab', { name: /google drive/i }).click()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { name: /import from google drive/i }))
|
||||
.toBeVisible()
|
||||
const panel = page.getByRole('tabpanel')
|
||||
await expect.element(panel.getByText('test-user@example.com')).toBeVisible()
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
await expect
|
||||
.element(page.getByRole('checkbox', { name: 'workspace' }))
|
||||
.toBeVisible()
|
||||
await expect
|
||||
.element(page.getByRole('checkbox', { name: 'readme.md' }))
|
||||
.toBeVisible()
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.uppy-ProviderBrowser-searchFilterInput',
|
||||
) as HTMLInputElement
|
||||
expect(searchInput).toBeDefined()
|
||||
|
||||
await userEvent.type(searchInput, 'workspace')
|
||||
await expect.element(page.getByText('workspace')).toBeVisible()
|
||||
|
||||
const visibleItems = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
|
||||
expect(visibleItems.length).toBe(1)
|
||||
const workspaceItem = visibleItems.find((item) =>
|
||||
item.textContent?.includes('workspace'),
|
||||
)
|
||||
expect(workspaceItem).toBeTruthy()
|
||||
|
||||
const firstItem = visibleItems.find((item) => {
|
||||
const button = item.querySelector('button.uppy-ProviderBrowserItem-inner')
|
||||
return button?.textContent?.trim() === 'first'
|
||||
})
|
||||
const readmeItem = visibleItems.find((item) =>
|
||||
item.textContent?.includes('readme.md'),
|
||||
)
|
||||
expect(firstItem).toBeUndefined()
|
||||
expect(readmeItem).toBeUndefined()
|
||||
|
||||
await userEvent.clear(searchInput)
|
||||
await expect
|
||||
.element(page.getByRole('button', { name: 'first' }))
|
||||
.toBeVisible()
|
||||
|
||||
const allItems = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
expect(allItems.length).toBe(3)
|
||||
|
||||
await userEvent.type(searchInput, 'readme')
|
||||
await expect
|
||||
.element(page.getByRole('checkbox', { name: 'readme.md' }))
|
||||
.toBeVisible()
|
||||
|
||||
const filteredItems = Array.from(
|
||||
document.querySelectorAll('.uppy-ProviderBrowserItem'),
|
||||
)
|
||||
expect(filteredItems.length).toBe(1)
|
||||
const readmeFiltered = filteredItems.find((item) =>
|
||||
item.textContent?.includes('readme.md'),
|
||||
)
|
||||
expect(readmeFiltered).toBeTruthy()
|
||||
})
|
||||
})
|
||||
447
packages/@uppy/dashboard/src/mocks/CompanionHandler.ts
Normal file
447
packages/@uppy/dashboard/src/mocks/CompanionHandler.ts
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
import { HttpResponse, http } from 'msw'
|
||||
|
||||
const COMPANION_URL = 'http://companion.test'
|
||||
|
||||
/**
|
||||
* Mocked Folder structure :
|
||||
*
|
||||
|
||||
root/ (Dropbox)
|
||||
├── first/
|
||||
│ ├── second/
|
||||
│ │ ├── third/
|
||||
│ │ │ ├── nested-target.pdf
|
||||
│ │ │ └── new-file.pdf
|
||||
│ │ ├── deep-file.txt
|
||||
│ │ ├── target.pdf
|
||||
│ │ └── workspace.pdf
|
||||
│ └── intermediate.doc
|
||||
├── workspace/
|
||||
│ └── project/
|
||||
│ └── code.js
|
||||
└── readme.md
|
||||
|
||||
*/
|
||||
|
||||
export const handlers = [
|
||||
// Mock pre-auth token
|
||||
http.get(`${COMPANION_URL}/:provider/preauth`, () => {
|
||||
return HttpResponse.json({ token: 'mock-preauth-token' })
|
||||
}),
|
||||
|
||||
// Mock authentication check
|
||||
http.get(`${COMPANION_URL}/:provider/connect`, () => {
|
||||
return HttpResponse.json({
|
||||
authenticated: true,
|
||||
username: 'test-user@example.com',
|
||||
})
|
||||
}),
|
||||
|
||||
// Mock folder listing endpoint
|
||||
http.get(`${COMPANION_URL}/:provider/list/*`, ({ request }) => {
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Extract path: split by '/list/' and take everything after it
|
||||
const [, afterList] = url.pathname.split('/list/')
|
||||
const pathStr = afterList ? decodeURIComponent(afterList) : ''
|
||||
|
||||
// Root folder
|
||||
if (!pathStr || pathStr === 'root') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'first',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-first',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first'),
|
||||
modifiedDate: '2024-01-01T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'workspace',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-workspace',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace'),
|
||||
modifiedDate: '2024-01-02T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'readme.md',
|
||||
mimeType: 'text/markdown',
|
||||
id: 'file-readme',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/readme.md'),
|
||||
modifiedDate: '2024-01-03T00:00:00Z',
|
||||
size: 1024,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first folder
|
||||
if (pathStr === '/first') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'second',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-second',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second'),
|
||||
modifiedDate: '2024-01-04T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'intermediate.doc',
|
||||
mimeType: 'application/msword',
|
||||
id: 'file-intermediate',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/intermediate.doc'),
|
||||
modifiedDate: '2024-01-05T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first/second folder (deep nested)
|
||||
if (pathStr === '/first/second') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'third',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-third',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/third'),
|
||||
modifiedDate: '2024-01-06T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'deep-file.txt',
|
||||
mimeType: 'text/plain',
|
||||
id: 'file-deep',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/deep-file.txt'),
|
||||
modifiedDate: '2024-01-07T00:00:00Z',
|
||||
size: 512,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'workspace.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-workspace-pdf',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/workspace.pdf'),
|
||||
modifiedDate: '2024-01-11T00:00:00Z',
|
||||
size: 5120,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// first/second/third folder (deepest level)
|
||||
if (pathStr === '/first/second/third') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'new-file.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-new',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/third/new-file.pdf'),
|
||||
modifiedDate: '2024-01-10T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// workspace folder
|
||||
if (pathStr === '/workspace') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'project',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-project',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// workspace/project folder
|
||||
if (pathStr === '/workspace/project') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'code.js',
|
||||
mimeType: 'application/javascript',
|
||||
id: 'file-code',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project/code.js'),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}
|
||||
|
||||
// Default empty folder
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [],
|
||||
nextPagePath: null,
|
||||
})
|
||||
}),
|
||||
|
||||
// Mock search endpoint
|
||||
http.get(`${COMPANION_URL}/:provider/search`, ({ request, params }) => {
|
||||
const url = new URL(request.url)
|
||||
const provider = params.provider as string
|
||||
const query = url.searchParams.get('q') || ''
|
||||
const searchPath = url.searchParams.get('path')
|
||||
|
||||
if (provider === 'drive') {
|
||||
return HttpResponse.json(
|
||||
{ message: 'method not implemented' },
|
||||
{ status: 500 },
|
||||
)
|
||||
}
|
||||
|
||||
// Search for "second" folder
|
||||
if (query.toLowerCase() === 'second') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: true,
|
||||
icon: 'folder',
|
||||
name: 'second',
|
||||
mimeType: 'folder',
|
||||
id: 'folder-second',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second'),
|
||||
modifiedDate: '2024-01-04T00:00:00Z',
|
||||
size: null,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "target" file (global search from root)
|
||||
if (query.toLowerCase() === 'target' && !searchPath) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Scoped search for "target" from /first directory
|
||||
if (
|
||||
query.toLowerCase() === 'target' &&
|
||||
searchPath === encodeURIComponent('/first')
|
||||
) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/target.pdf'),
|
||||
modifiedDate: '2024-01-08T00:00:00Z',
|
||||
size: 4096,
|
||||
},
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'nested-target.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
id: 'file-nested-target',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent(
|
||||
'/first/second/third/nested-target.pdf',
|
||||
),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "deep" - deep nested file
|
||||
if (query.toLowerCase() === 'deep') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'deep-file.txt',
|
||||
mimeType: 'text/plain',
|
||||
id: 'file-deep',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/first/second/deep-file.txt'),
|
||||
modifiedDate: '2024-01-07T00:00:00Z',
|
||||
size: 512,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search from subpath - "code" in workspace
|
||||
if (
|
||||
query.toLowerCase() === 'code' &&
|
||||
searchPath === encodeURIComponent('/workspace')
|
||||
) {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'code.js',
|
||||
mimeType: 'application/javascript',
|
||||
id: 'file-code',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/workspace/project/code.js'),
|
||||
modifiedDate: '2024-01-09T00:00:00Z',
|
||||
size: 3072,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// Search for "readme" file in root
|
||||
if (query.toLowerCase() === 'readme') {
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [
|
||||
{
|
||||
isFolder: false,
|
||||
icon: 'file',
|
||||
name: 'readme.md',
|
||||
mimeType: 'text/markdown',
|
||||
id: 'file-readme',
|
||||
thumbnail: null,
|
||||
requestPath: encodeURIComponent('/readme.md'),
|
||||
modifiedDate: '2024-01-03T00:00:00Z',
|
||||
size: 1024,
|
||||
},
|
||||
],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}
|
||||
|
||||
// No results
|
||||
return HttpResponse.json({
|
||||
username: 'test-user@example.com',
|
||||
items: [],
|
||||
nextPagePath: null,
|
||||
searchedFor: query,
|
||||
})
|
||||
}),
|
||||
]
|
||||
4
packages/@uppy/dashboard/src/setup.ts
Normal file
4
packages/@uppy/dashboard/src/setup.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { setupWorker } from 'msw/browser'
|
||||
import { handlers } from '../src/mocks/CompanionHandler.js'
|
||||
|
||||
export const worker = setupWorker(...handlers)
|
||||
|
|
@ -12,6 +12,12 @@ import type { ComponentChild, h } from 'preact'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
DragDrop: DragDrop<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export interface DragDropOptions extends UIPluginOptions {
|
||||
inputName?: string
|
||||
allowMultipleFiles?: boolean
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@ import { BasePlugin } from '@uppy/core'
|
|||
import { getDroppedFiles, toArray } from '@uppy/utils'
|
||||
import packageJson from '../package.json' with { type: 'json' }
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
DropTarget: DropTarget<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export interface DropTargetOptions extends PluginOpts {
|
||||
target?: HTMLElement | string | null
|
||||
onDrop?: (event: DragEvent) => void
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import { type ComponentChild, h } from 'preact'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Dropbox: Dropbox<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type DropboxOptions = CompanionPluginOptions & {
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import { type ComponentChild, h } from 'preact'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Facebook: Facebook<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type FacebookOptions = CompanionPluginOptions & {
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ import getFormData from 'get-form-data'
|
|||
|
||||
import packageJson from '../package.json' with { type: 'json' }
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Form: Form<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
type Result<M extends Meta, B extends Body> = Parameters<
|
||||
UppyEventMap<M, B>['complete']
|
||||
>[0]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ function createUppy(
|
|||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
return new Uppy().use(Dashboard, {
|
||||
return new Uppy({
|
||||
debug: true,
|
||||
}).use(Dashboard, {
|
||||
target: root,
|
||||
inline: true,
|
||||
...opts,
|
||||
|
|
@ -36,9 +38,11 @@ beforeEach(async () => {
|
|||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
let fileIndex = 0
|
||||
|
||||
const createMockFile = ({
|
||||
size,
|
||||
name = `${Date.now()}.txt`,
|
||||
name = `${fileIndex++}.txt`,
|
||||
type = 'text/plain',
|
||||
}: {
|
||||
name?: string
|
||||
|
|
@ -99,7 +103,9 @@ describe('Golden retriever', () => {
|
|||
requestAt += 1
|
||||
return HttpResponse.json({})
|
||||
}
|
||||
// never reply to subsequent requests (leave them hanging)
|
||||
// fail subsequent requests
|
||||
requestAt += 1
|
||||
return HttpResponse.json({}, { status: 400 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -117,10 +123,15 @@ describe('Golden retriever', () => {
|
|||
uppy.once('upload-success', () => resolve()),
|
||||
)
|
||||
|
||||
const uploadCompletePromise = new Promise<void>((resolve) =>
|
||||
uppy.once('complete', () => resolve()),
|
||||
)
|
||||
|
||||
// Start the upload
|
||||
await page.getByRole('button', { name: 'Upload 2 files' }).click()
|
||||
|
||||
await uploadFirstFileCompletePromise
|
||||
await uploadCompletePromise
|
||||
|
||||
// reload page and recreate Uppy instance
|
||||
uppy = createUppy({ withPageReload: true })
|
||||
|
|
@ -140,7 +151,8 @@ describe('Golden retriever', () => {
|
|||
return HttpResponse.json({})
|
||||
}
|
||||
// don't allow more than 1 request
|
||||
return new HttpResponse({ status: 400 })
|
||||
requestAt += 1
|
||||
return HttpResponse.json({}, { status: 400 })
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -152,6 +164,7 @@ describe('Golden retriever', () => {
|
|||
.toBeVisible()
|
||||
|
||||
expect(uppy.getFiles().length).toBe(2)
|
||||
expect(requestAt).toBe(1) // only the first file upload should have happened so far
|
||||
})
|
||||
|
||||
test('Should not clean up files upon completion if there were failed uploads and it should only make the failed file a ghost', async ({
|
||||
|
|
@ -207,9 +220,9 @@ describe('Golden retriever', () => {
|
|||
|
||||
// Simulate ghosting of the files by deleting it from store(s)
|
||||
// @ts-expect-error
|
||||
;(uppy.getPlugin('GoldenRetriever') as GoldenRetriever<any, any>)[
|
||||
Symbol.for('uppy test: deleteBlobs')
|
||||
](fileIds)
|
||||
uppy
|
||||
.getPlugin('GoldenRetriever')
|
||||
[Symbol.for('uppy test: deleteBlobs')](fileIds)
|
||||
|
||||
// reload page and recreate Uppy instance
|
||||
uppy = createUppy({ withPageReload: true }).use(GoldenRetriever)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ declare module '@uppy/core' {
|
|||
export interface UppyEventMap<M extends Meta, B extends Body> {
|
||||
'restore:plugin-data-changed': (data: Record<string, unknown>) => void
|
||||
}
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
GoldenRetriever: GoldenRetriever<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoldenRetrieverOptions extends PluginOpts {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import type { LocaleStrings } from '@uppy/utils'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
GoogleDrivePicker: GoogleDrivePicker<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type GoogleDrivePickerOptions = CompanionPluginOptions & {
|
||||
clientId: string
|
||||
apiKey: string
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ import packageJson from '../package.json' with { type: 'json' }
|
|||
import DriveProviderViews from './DriveProviderViews.js'
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
GoogleDrive: GoogleDrive<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type GoogleDriveOptions = CompanionPluginOptions & {
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import type { LocaleStrings } from '@uppy/utils'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
GooglePhotosPicker: GooglePhotosPicker<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type GooglePhotosPickerOptions = CompanionPluginOptions & {
|
||||
clientId: string
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import packageJson from '../package.json' with { type: 'json' }
|
|||
import Editor from './Editor.js'
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
ImageEditor: ImageEditor<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace preact {
|
||||
interface Component {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ import { type ComponentChild, h } from 'preact'
|
|||
import packageJson from '../package.json' with { type: 'json' }
|
||||
import locale from './locale.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Instagram: Instagram<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
export type InstagramOptions = CompanionPluginOptions & {
|
||||
locale?: LocaleStrings<typeof locale>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,152 +15,185 @@ nl_NL.strings = {
|
|||
'0': 'Toevoegen van %{smart_count} bestand mislukt door een interne fout',
|
||||
'1': 'Toevoegen van %{smart_count} bestanden mislukt door interne fouten',
|
||||
},
|
||||
addedNumFiles: '%{numFiles} bestand(en) toegevoegd',
|
||||
addingMoreFiles: 'Bezig met extra bestanden toe te voegen',
|
||||
additionalRestrictionsFailed:
|
||||
'Er werd niet voldaan aan %{count} additionele restricties',
|
||||
addMore: 'Meer toevoegen',
|
||||
addMoreFiles: 'Extra bestanden toevoegen',
|
||||
addMoreFiles: 'Meer bestanden toevoegen',
|
||||
aggregateExceedsSize:
|
||||
'Je selecteerde %{size} aan bestanden, maar maximaal %{sizeAllowed} is toegestaan',
|
||||
allFilesFromFolderNamed: 'Alle bestanden uit de map %{name}',
|
||||
allowAccessDescription:
|
||||
"Geef toestemming om foto's of videobeelden te kunnen maken.",
|
||||
allowAccessTitle: 'Geef toestemming om je camera te gebruiken',
|
||||
"Geef deze site toegang tot je camera om foto's of videobeelden te kunnen maken.",
|
||||
allowAccessTitle: 'Geef toegang tot je camera',
|
||||
allowAudioAccessDescription:
|
||||
'Om geluidsopnamen te maken moet U toestemming geven voor deze website',
|
||||
allowAudioAccessTitle: 'Geef toestemming om uw microfoon te gebruiken',
|
||||
'Geef deze site toegang tot je microfoon om geluidsopnames te maken.',
|
||||
allowAudioAccessTitle: 'Geef toegang tot je microfoon',
|
||||
aspectRatioLandscape: 'Landschap knippen (16:9)',
|
||||
aspectRatioPortrait: 'Portret knippen (9:16)',
|
||||
aspectRatioSquare: 'Vierkant knippen',
|
||||
authAborted: 'Authenticatie geannuleerd',
|
||||
authenticate: 'Verbinden',
|
||||
authenticateWith: 'Verbinden met %{pluginName}',
|
||||
authenticateWithTitle: 'Verbind met %{pluginName} om bestanden te selecteren',
|
||||
back: 'Terug',
|
||||
browse: 'blader',
|
||||
browseFiles: 'blader',
|
||||
browseFolders: 'blader',
|
||||
browseFiles: 'blader naar bestanden',
|
||||
browseFolders: 'blader naar mappen',
|
||||
cancel: 'Annuleer',
|
||||
cancelUpload: 'Annuleer upload',
|
||||
closeModal: 'Sluit Venster',
|
||||
closeModal: 'Sluit venster',
|
||||
companionError: 'Verbinding met Companion mislukt',
|
||||
companionUnauthorizeHint:
|
||||
'Om toegang te ontnemen voor uw %{provider} account, ga naar %{url}',
|
||||
'Om toegang te ontnemen voor je %{provider} account, ga naar %{url}',
|
||||
complete: 'Voltooid',
|
||||
compressedX: '%{size} bespaard door het comprimeren van afbeeldingen',
|
||||
compressedX: '%{size} bespaard door afbeeldingen te comprimeren',
|
||||
compressingImages: 'Afbeeldingen aan het comprimeren...',
|
||||
connectedToInternet: 'Verbonden met het internet',
|
||||
copyLink: 'Kopieer link',
|
||||
copyLinkToClipboardFallback: 'Kopieer de onderstaande URL',
|
||||
copyLinkToClipboardSuccess: 'Link naar klembord gekopieerd',
|
||||
creatingAssembly: 'Upload voorbereiden...',
|
||||
creatingAssemblyFailed: 'Transloadit: Kon Assembly niet creëeren',
|
||||
creatingAssemblyFailed: 'Transloadit: Kon Assembly niet aanmaken',
|
||||
dashboardTitle: 'Uppy Dashboard',
|
||||
dashboardWindowTitle: 'Uppy Dashboard Venster (Druk escape om te sluiten)',
|
||||
dataUploadedOfTotal: '%{complete} van %{total}',
|
||||
dataUploadedOfUnknown: '%{complete} van onbekend',
|
||||
discardMediaFile: 'Verwijder Media',
|
||||
discardRecordedFile: 'Verwijder opgenomen bestand',
|
||||
done: 'Klaar',
|
||||
dropHint: 'Sleep hier je bestanden naartoe',
|
||||
dropPasteBoth: 'Sleep hier je bestanden naartoe, plak of %{browse}',
|
||||
dropPasteFiles: 'Sleep hier je bestanden naartoe, plak of %{browse}',
|
||||
dropPasteFolders: 'Sleep hier je bestanden naartoe, plak of %{browse}',
|
||||
dropHereOr: 'Sleep bestanden hier heen of %{browse}',
|
||||
dropHint: 'Sleep bestanden hier heen',
|
||||
dropPasteBoth:
|
||||
'Sleep bestanden hier heen, %{browseFiles} of %{browseFolders}',
|
||||
dropPasteFiles: 'Sleep bestanden hier heen of %{browseFiles}',
|
||||
dropPasteFolders: 'Sleep bestanden hier heen of %{browseFolders}',
|
||||
dropPasteImportBoth:
|
||||
'Sleep hier je bestanden naartoe, plak, %{browse} of importeer vanuit',
|
||||
'Sleep bestanden hier heen, %{browseFiles}, %{browseFolders} of importeer vanuit:',
|
||||
dropPasteImportFiles:
|
||||
'Sleep hier je bestanden naartoe, plak, %{browse} of importeer vanuit',
|
||||
'Sleep bestanden hier heen, %{browseFiles} of importeer vanuit:',
|
||||
dropPasteImportFolders:
|
||||
'Sleep hier je bestanden naartoe, plak, %{browse} of importeer vanuit',
|
||||
editFile: 'Bestand aanpassen',
|
||||
editImage: 'Afbeelding aanpassen',
|
||||
editFileWithFilename: 'Bestand aanpassen %{file}',
|
||||
editing: 'Bezig %{file} aan te passen',
|
||||
'Sleep bestanden hier heen, %{browseFolders} of importeer vanuit:',
|
||||
editFile: 'Bestand wijzigen',
|
||||
editFileWithFilename: 'Wijzig bestand %{file}',
|
||||
editImage: 'Afbeelding wijzigen',
|
||||
editing: 'Bezig %{file} te wijzigen',
|
||||
emptyFolderAdded: 'Er werden geen bestanden toegevoegd uit de lege map',
|
||||
encoding: 'Coderen...',
|
||||
encoding: 'Encoderen...',
|
||||
enterCorrectUrl:
|
||||
'Ongeldige URL: Zorg dat je een directe link naar een bestand invoert',
|
||||
enterTextToSearch: 'Type hier om te zoeken naar plaatjes',
|
||||
enterUrlToImport: 'Voeg URL toe om een bestand te importeren',
|
||||
enterTextToSearch: 'Type hier om te zoeken naar afbeeldingen',
|
||||
enterUrlToImport: 'Voer een URL in om een bestand te importeren',
|
||||
error: 'Fout',
|
||||
exceedsSize:
|
||||
'%{file} overschrijdt de maximaal toegelaten bestandsgrootte van %{size}',
|
||||
failedToFetch:
|
||||
'Companion kan deze URL niet laden, controleer of de URL correct is',
|
||||
failedToUpload: 'Kon %{file} niet uploaden',
|
||||
fileSource: 'Bronbestand: %{name}',
|
||||
filesUploadedOfTotal: {
|
||||
'0': '%{complete} van %{smart_count} bestand geüpload',
|
||||
'1': '%{complete} van %{smart_count} bestanden geüpload',
|
||||
},
|
||||
filter: 'Filter',
|
||||
finishEditingFile: 'Klaar met bestand aan te passen',
|
||||
flipHorizontal: 'Flip horizontaal',
|
||||
finishEditingFile: 'Klaar met bestand te wijzigen',
|
||||
flipHorizontal: 'Horizontaal spiegelen',
|
||||
folderAdded: {
|
||||
'0': '%{smart_count} bestand uit %{folder} toegevoegd',
|
||||
'1': '%{smart_count} bestanden uit %{folder} toegevoegd',
|
||||
'0': '%{smart_count} bestand toegevoegd uit %{folder}',
|
||||
'1': '%{smart_count} bestanden toegevoegd uit %{folder}',
|
||||
},
|
||||
folderAlreadyAdded: 'De map "%{folder}" is al toegevoegd',
|
||||
folderAlreadyAdded: 'De map "%{folder}" werd reeds toegevoegd',
|
||||
generatingThumbnails: 'Thumbnails genereren...',
|
||||
import: 'Importeer',
|
||||
importFiles: 'Importeer bestanden van:',
|
||||
importFrom: 'Importeer vanuit %{name}',
|
||||
inferiorSize: 'Dit bestand is kleiner dat de minimale grootte van %{size}',
|
||||
importFiles: 'Bestanden importeren van:',
|
||||
importFrom: 'Bestanden importeren vanuit %{name}',
|
||||
inferiorSize: 'Dit bestand is kleiner dan de minimum grootte van %{size}',
|
||||
loadedXFiles: '%{numFiles} bestanden geladen',
|
||||
loading: 'Bezig met laden...',
|
||||
logIn: 'Inloggen',
|
||||
logOut: 'Uitloggen',
|
||||
micDisabled: 'Microfoon toegang geweigerd door de gebruiker',
|
||||
missingRequiredMetaField: 'De vereiste metadata ontbreekt',
|
||||
missingRequiredMetaFieldOnFile:
|
||||
'De vereiste metadata ontbreekt voor %{fileName}',
|
||||
micDisabled: 'Microfoon toegang geweigerd door gebruiker',
|
||||
missingRequiredMetaField: 'Vereiste metadata ontbreekt',
|
||||
missingRequiredMetaFieldOnFile: 'Vereiste metadata ontbreekt in %{fileName}',
|
||||
missingRequiredMetaFields: {
|
||||
'0': 'Deze vereiste metadata ontbreekt: %{fields}.',
|
||||
'1': 'Deze vereiste metadata ontbreekt: %{fields}.',
|
||||
'0': 'Ontbrekende metadata: %{fields}.',
|
||||
'1': 'Ontbrekende metadata: %{fields}.',
|
||||
},
|
||||
myDevice: 'Mijn apparaat',
|
||||
noAudioDescription:
|
||||
'Om audio op te nemen, sluit een microfoon of andere geluidsinput aan',
|
||||
'Sluit een microfoon of andere geluidsinput aan om een geluidsopname te maken',
|
||||
noAudioTitle: 'Microfoon niet beschikbaar',
|
||||
noCameraDescription:
|
||||
"Om foto's en/of video op te nemen, sluit een camera aan",
|
||||
"Sluit een camera aan om foto's of videobeelden te maken",
|
||||
noCameraTitle: 'Camera niet beschikbaar',
|
||||
noDuplicates:
|
||||
"Kan het dubbele bestand '%{fileName}' niet toevoegen, deze bestaat al",
|
||||
"Kan het dubbele bestand '%{fileName}' niet toevoegen, het bestaat al",
|
||||
noFilesFound: 'Geen bestanden of mappen gevonden',
|
||||
noInternetConnection: 'Geen internetverbinding',
|
||||
noMoreFilesAllowed: 'Meer bestanden kunnen niet worden toegevoegd',
|
||||
noInternetConnection: 'Geen verbinding met het internet',
|
||||
noMoreFilesAllowed: 'Meer bestanden kunnen niet toegevoegd worden',
|
||||
noSearchResults: 'Deze zoekopdracht leverde helaas geen resultaten op',
|
||||
openFolderNamed: 'Open map %{name}',
|
||||
pause: 'Pauze',
|
||||
paused: 'Gepauzeerd',
|
||||
pauseUpload: 'Pauzeer upload',
|
||||
pauseUpload: 'Upload pauzeren',
|
||||
pickFiles: 'Selecteer bestanden',
|
||||
pickPhotos: "Selecteer foto's",
|
||||
pleaseWait: 'Gelieve te wachten',
|
||||
pluginNameAudio: 'Audio',
|
||||
pluginNameBox: 'Box',
|
||||
pluginNameCamera: 'Camera',
|
||||
pluginNameDropbox: 'Dropbox',
|
||||
pluginNameFacebook: 'Facebook',
|
||||
pluginNameGoogleDrive: 'Google Drive',
|
||||
pluginNameGoogleDrivePicker: 'Google Drive',
|
||||
pluginNameGooglePhotosPicker: 'Google Photos',
|
||||
pluginNameInstagram: 'Instagram',
|
||||
pluginNameOneDrive: 'OneDrive',
|
||||
pluginNameScreenCapture: 'Screencast',
|
||||
pluginNameUnsplash: 'Unsplash',
|
||||
pluginNameUrl: 'Link',
|
||||
pluginNameWebdav: 'WebDAV',
|
||||
pluginNameZoom: 'Zoom',
|
||||
pluginWebdavInputLabel:
|
||||
'WebDAV URL naar een bestand (bijv. van ownCloud of Nextcloud)',
|
||||
poweredBy: 'Mogelijk gemaakt door %{uppy}',
|
||||
processingXFiles: {
|
||||
'0': 'Bezig met %{smart_count} bestand te verwerken',
|
||||
'1': 'Bezig met %{smart_count} bestanden te verwerken',
|
||||
'0': 'Bezig met verwerken van %{smart_count} bestand',
|
||||
'1': 'Bezig met verwerken van %{smart_count} bestanden',
|
||||
},
|
||||
recording: 'Aan het opnemen',
|
||||
recordingLength: 'Opnameduur %{recording_length}',
|
||||
recordingStoppedMaxSize:
|
||||
'Opname gestopt omdat de bestandsgrootte de limiet bijna overschrijdt',
|
||||
recordVideoBtn: 'Video opnemen',
|
||||
recoveredAllFiles:
|
||||
'Alle bestanden zijn herstel. U kunt doorgaan me de upload.',
|
||||
'We hebben alle bestanden hersteld. Je kan de upload nu hervatten.',
|
||||
recoveredXFiles: {
|
||||
'0': 'We konden 1 bestand niet herstellen. Kies het bestand opnieuw en vervolg de upload',
|
||||
'1': 'We konden %{smart_count} bestanden niet herstellen. Kies de bestanden opnieuw en vervolg de upload.',
|
||||
'0': 'We konden 1 bestand niet herstellen. Kies het bestand opnieuw en hervat de upload.',
|
||||
'1': 'We konden %{smart_count} bestanden niet herstellen. Kies de bestanden opnieuw en hervat de upload.',
|
||||
},
|
||||
removeFile: 'Verwijder bestand %{file}',
|
||||
removeFile: 'Verwijder bestand',
|
||||
reSelect: 'Opnieuw selecteren',
|
||||
resetFilter: 'Filter resetten',
|
||||
resetSearch: 'Zoeken resetten',
|
||||
resume: 'Hervatten',
|
||||
resumeUpload: 'Upload hervatten',
|
||||
retry: 'Opnieuw',
|
||||
retryUpload: 'Upload opnieuw',
|
||||
retry: 'Opnieuw proberen',
|
||||
retryUpload: 'Upload opnieuw proberen',
|
||||
revert: 'Terugdraaien',
|
||||
rotate: 'Draai',
|
||||
rotate: 'Draai 90°',
|
||||
save: 'Opslaan',
|
||||
saveChanges: 'Wijzigingen opslaan',
|
||||
search: 'Zoek',
|
||||
searchImages: 'Zoek naar plaatjes',
|
||||
selectFileNamed: 'Selecteer bestand %{name}',
|
||||
search: 'Zoeken',
|
||||
searchImages: "Foto's zoeken",
|
||||
selectX: {
|
||||
'0': 'Selecteer %{smart_count}',
|
||||
'1': 'Selecteer %{smart_count}',
|
||||
},
|
||||
sessionRestored: 'Sessie hersteld',
|
||||
showErrorDetails: 'Laat fout details zien',
|
||||
showErrorDetails: 'Toon foutdetails',
|
||||
signInWithGoogle: 'Inloggen met Google',
|
||||
smile: 'Lach!',
|
||||
smile: 'Lachen!',
|
||||
startAudioRecording: 'Start audio-opname',
|
||||
startCapturing: 'Start scherm-opname',
|
||||
startRecording: 'Start video-opname',
|
||||
|
|
@ -171,18 +204,20 @@ nl_NL.strings = {
|
|||
streamPassive: 'Stream passief',
|
||||
submitRecordedFile: 'Stuur opgenomen bestand',
|
||||
takePicture: 'Neem een foto',
|
||||
timedOut:
|
||||
'Upload al gedurende %{seconds} seconden vastgelopen, bezig afbreken upload.',
|
||||
unselectFileNamed: 'Deselecteer bestand %{name}',
|
||||
takePictureBtn: 'Foto nemen',
|
||||
takeScreenshot: 'Screenshot maken',
|
||||
unnamed: 'Onbekend',
|
||||
upload: 'Upload',
|
||||
uploadComplete: 'Upload voltooid',
|
||||
uploadFailed: 'Upload mislukt',
|
||||
uploading: 'Bezig met uploaden',
|
||||
uploading: 'Aan het uploaden',
|
||||
uploadingXFiles: {
|
||||
'0': 'Bezig met %{smart_count} bestand te uploaden',
|
||||
'1': 'Bezig met %{smart_count} bestanden te uploaden',
|
||||
'0': '%{smart_count} bestand aan het uploaden',
|
||||
'1': '%{smart_count} bestanden aan het uploaden',
|
||||
},
|
||||
uploadPaused: 'Upload gepauzeerd',
|
||||
uploadStalled:
|
||||
'Upload maakte geen vordering gedurende %{seconds} seconden. Probeer opnieuw.',
|
||||
uploadXFiles: {
|
||||
'0': 'Upload %{smart_count} bestand',
|
||||
'1': 'Upload %{smart_count} bestanden',
|
||||
|
|
@ -199,7 +234,7 @@ nl_NL.strings = {
|
|||
'0': '%{smart_count} extra bestand toegevoegd',
|
||||
'1': '%{smart_count} extra bestanden toegevoegd',
|
||||
},
|
||||
xTimeLeft: '%{time} over',
|
||||
xTimeLeft: '%{time} resterend',
|
||||
youCanOnlyUploadFileTypes: 'Je kan enkel volgende types uploaden: %{types}',
|
||||
youCanOnlyUploadX: {
|
||||
'0': 'Je kan slechts %{smart_count} bestand uploaden',
|
||||
|
|
|
|||
|
|
@ -9,23 +9,38 @@ const zh_CN: Locale<0> = {
|
|||
|
||||
zh_CN.strings = {
|
||||
addBulkFilesFailed: '内部错误导致添加 %{smart_count} 个文件失败',
|
||||
addedNumFiles: '已添加 %{numFiles} 个文件',
|
||||
addingMoreFiles: '添加更多文件',
|
||||
additionalRestrictionsFailed: '有 %{count} 个附加限制未满足',
|
||||
addMore: '添加更多文件',
|
||||
addMoreFiles: '添加更多文件',
|
||||
addingMoreFiles: '添加更多文件',
|
||||
aggregateExceedsSize:
|
||||
'您选择了 %{size} 的文件,但最大允许大小为 %{sizeAllowed}',
|
||||
allFilesFromFolderNamed: '来自文件夹 %{name} 的所有文件',
|
||||
allowAccessDescription:
|
||||
'为了通过您的相机进行拍照或录像,请给网站相机的访问权限',
|
||||
allowAccessTitle: '请允许对相机的访问权限',
|
||||
allowAudioAccessDescription: '为了录制音频,请允许网站访问麦克风',
|
||||
allowAudioAccessTitle: '请允许访问麦克风',
|
||||
aspectRatioLandscape: '裁剪为横向(16:9)',
|
||||
aspectRatioPortrait: '裁剪为纵向(9:16)',
|
||||
aspectRatioSquare: '裁剪为正方形',
|
||||
authAborted: '认证已中止',
|
||||
authenticate: '连接',
|
||||
authenticateWith: '连接到 %{pluginName}',
|
||||
authenticateWithTitle: '请使用 %{pluginName} 进行认证以选择文件',
|
||||
back: '返回',
|
||||
browse: '浏览',
|
||||
browseFiles: '浏览',
|
||||
browseFiles: '浏览文件',
|
||||
browseFolders: '浏览文件夹',
|
||||
cancel: '取消',
|
||||
cancelUpload: '取消上传',
|
||||
closeModal: '关闭窗口',
|
||||
companionError: '和 Companion 连接失败了',
|
||||
companionUnauthorizeHint: '请访问 %{url} 以认证您的 %{provider} 账户',
|
||||
complete: '上传完毕',
|
||||
compressedX: '通过压缩图片节省了 %{size}',
|
||||
compressingImages: '正在压缩图片…',
|
||||
connectedToInternet: '连接至网络',
|
||||
copyLink: '复制链接',
|
||||
copyLinkToClipboardFallback: '复制以下网址',
|
||||
|
|
@ -33,88 +48,134 @@ zh_CN.strings = {
|
|||
creatingAssembly: '正在准备上传…',
|
||||
creatingAssemblyFailed: 'Transloadit:无法创建程序集',
|
||||
dashboardTitle: '文件上传工具',
|
||||
dashboardWindowTitle: '文件上传工具窗口(点击离开以关闭)',
|
||||
dataUploadedOfTotal: '%{total} / %{complete}',
|
||||
dashboardWindowTitle: '文件上传工具窗口(按 Escape 键关闭)',
|
||||
dataUploadedOfTotal: '%{complete} / %{total}',
|
||||
dataUploadedOfUnknown: '%{complete} / 未知大小',
|
||||
discardMediaFile: '丢弃媒体文件',
|
||||
discardRecordedFile: '丢弃录制的文件',
|
||||
done: '完成',
|
||||
dropHereOr: '将文件拖拽到这里或者%{browse}',
|
||||
dropHint: '拖拽文件到这里',
|
||||
dropPasteBoth: '拖拽文件到这里,或者%{browse}文件',
|
||||
dropPasteFiles: '拖拽文件到这里,或者%{browse}文件',
|
||||
dropPasteFolders: '拖拽文件到这里,或者%{browse}文件',
|
||||
dropPasteImportBoth: '拖拽文件到这里,粘贴、%{browse}或者导入',
|
||||
dropPasteImportFiles: '拖拽文件到这里,粘贴、%{browse}或者导入',
|
||||
dropPasteImportFolders: '拖拽文件到这里,粘贴、%{browse}或者导入',
|
||||
dropPasteBoth: '将文件拖拽到这里,%{browseFiles}或%{browseFolders}',
|
||||
dropPasteFiles: '将文件拖拽到这里或%{browseFiles}',
|
||||
dropPasteFolders: '将文件拖拽到这里或%{browseFolders}',
|
||||
dropPasteImportBoth:
|
||||
'将文件拖拽到这里,%{browseFiles}、%{browseFolders}或从以下位置导入:',
|
||||
dropPasteImportFiles: '将文件拖拽到这里,%{browseFiles}或从以下位置导入:',
|
||||
dropPasteImportFolders:
|
||||
'将文件拖拽到这里,%{browseFolders}或从以下位置导入:',
|
||||
editFile: '编辑文件',
|
||||
editFileWithFilename: '编辑文件 %{file}',
|
||||
editImage: '编辑图片',
|
||||
editing: '正在编辑 %{file}',
|
||||
emptyFolderAdded: '无法从空文件夹添加文件',
|
||||
encoding: '正在编码…',
|
||||
enterCorrectUrl: '错误链接: 请确认您输入的是文件的链接',
|
||||
enterUrlToImport: '输入链接或者导入文件',
|
||||
exceedsSize: '文件超过了最大尺寸限制 %{size}',
|
||||
enterCorrectUrl: '错误链接:请确认您输入的是文件的直接链接',
|
||||
enterTextToSearch: '输入文字以搜索图片',
|
||||
enterUrlToImport: '输入链接以导入文件',
|
||||
error: '错误',
|
||||
exceedsSize: '%{file} 超过了最大允许大小 %{size}',
|
||||
failedToFetch: 'Companion 无法抓取此链接,请确保它是正确的',
|
||||
failedToUpload: '上传 %{file} 失败',
|
||||
fileSource: '文件源:%{name}',
|
||||
filesUploadedOfTotal: '已上传 %{smart_count} 个文件中的 %{complete} 个',
|
||||
filter: '筛选器',
|
||||
finishEditingFile: '完成文件编辑',
|
||||
flipHorizontal: '水平翻转',
|
||||
folderAdded: '从 %{folder} 添加了 %{smart_count} 个文件',
|
||||
folderAlreadyAdded: '文件夹"%{folder}"已经被添加过了',
|
||||
generatingThumbnails: '正在生成缩略图…',
|
||||
import: '导入',
|
||||
importFiles: '从以下位置导入文件:',
|
||||
importFrom: '从 %{name} 导入',
|
||||
inferiorSize: '文件大小必须超过 %{size}',
|
||||
inferiorSize: '此文件小于最小允许大小 %{size}',
|
||||
loadedXFiles: '已加载 %{numFiles} 个文件',
|
||||
loading: '正在载入…',
|
||||
logIn: '登录',
|
||||
logOut: '登出',
|
||||
micDisabled: '麦克风的权限访问被用户拒绝',
|
||||
missingRequiredMetaField: '缺少必需的元数据字段',
|
||||
missingRequiredMetaFieldOnFile: '%{fileName} 中缺少必需的元数据字段',
|
||||
missingRequiredMetaFields: '缺少必需的元数据字段:%{fields}',
|
||||
myDevice: '我的设备',
|
||||
noAudioDescription: '为了录制音频,请连接麦克风或其他音频输入设备',
|
||||
noAudioTitle: '麦克风不可用',
|
||||
noCameraDescription: '为了拍摄照片或录制视频,请连接一个摄像设备',
|
||||
noCameraTitle: '摄像头不可用',
|
||||
noDuplicates: '无法添加重复文件 %{fileName},该文件已存在',
|
||||
noDuplicates: '无法添加重复文件"%{fileName}",该文件已存在',
|
||||
noFilesFound: '这里空空如也',
|
||||
noInternetConnection: '无法连接到网络',
|
||||
noMoreFilesAllowed: '无法添加新文件:已正在上传文件',
|
||||
noMoreFilesAllowed: '无法添加更多文件',
|
||||
noSearchResults: '很抱歉,没有找到相关搜索结果',
|
||||
openFolderNamed: '打开文件夹 %{name}',
|
||||
pause: '暂停',
|
||||
pauseUpload: '暂停上传',
|
||||
paused: '已暂停',
|
||||
pauseUpload: '暂停上传',
|
||||
pickFiles: '选择文件',
|
||||
pickPhotos: '选择照片',
|
||||
pleaseWait: '请稍候',
|
||||
pluginNameAudio: '音频',
|
||||
pluginNameCamera: '相机',
|
||||
pluginNameScreenCapture: '屏幕录制',
|
||||
pluginNameUrl: '链接',
|
||||
pluginWebdavInputLabel: 'WebDAV 文件 URL(例如来自 ownCloud 或 Nextcloud)',
|
||||
poweredBy: '强力驱动于 %{uppy}',
|
||||
processingXFiles: '正在处理 %{smart_count} 个文件',
|
||||
recording: '正在录制',
|
||||
recordingLength: '录制长度 %{recording_length}',
|
||||
recordingStoppedMaxSize: '录像已停止,文件大小即将超过限制',
|
||||
recordingLength: '录制时长 %{recording_length}',
|
||||
recordingStoppedMaxSize: '录制已停止,文件大小即将超过限制',
|
||||
recordVideoBtn: '录制视频',
|
||||
recoveredAllFiles: '我们已恢复所有文件。您现在可以继续上传。',
|
||||
recoveredXFiles:
|
||||
'我们无法完全恢复 %{smart_count} 个文件。请重新选择它们并继续上传。',
|
||||
removeFile: '删除文件',
|
||||
reSelect: '重新选择',
|
||||
resetFilter: '重置筛选器',
|
||||
resetSearch: '重置搜索',
|
||||
resume: '恢复',
|
||||
resumeUpload: '恢复上传',
|
||||
retry: '重试',
|
||||
retryUpload: '重试',
|
||||
retryUpload: '重试上传',
|
||||
revert: '重置',
|
||||
rotate: '旋转 90°',
|
||||
save: '保存',
|
||||
saveChanges: '保存变更',
|
||||
selectFileNamed: '选择文件 %{name}',
|
||||
search: '搜索',
|
||||
searchImages: '搜索图片',
|
||||
selectX: '选择 %{smart_count}',
|
||||
sessionRestored: '会话已恢复',
|
||||
showErrorDetails: '显示错误详情',
|
||||
signInWithGoogle: '使用 Google 登录',
|
||||
smile: '笑一笑!',
|
||||
startAudioRecording: '开始音频录制',
|
||||
startCapturing: '开始屏幕录制',
|
||||
startRecording: '开始视频录制',
|
||||
stopAudioRecording: '停止音频录制',
|
||||
stopCapturing: '停止屏幕录制',
|
||||
stopRecording: '停止视频录制',
|
||||
streamActive: '视频流已激活',
|
||||
streamPassive: '视频流未激活',
|
||||
submitRecordedFile: '提交已录制视频',
|
||||
submitRecordedFile: '提交录制的文件',
|
||||
takePicture: '拍照',
|
||||
timedOut: '上传已超时 %{seconds} 秒,中止上传',
|
||||
unselectFileNamed: '取消选择文件 %{name}',
|
||||
takePictureBtn: '拍照',
|
||||
takeScreenshot: '截屏',
|
||||
unnamed: '未命名',
|
||||
upload: '上传',
|
||||
uploadComplete: '上传完成',
|
||||
uploadFailed: '上传失败',
|
||||
uploadPaused: '上传暂停',
|
||||
uploadXFiles: '上传 %{smart_count} 个文件',
|
||||
uploadXNewFiles: '新上传了 %{smart_count} 个文件',
|
||||
uploading: '正在上传',
|
||||
uploadingXFiles: '正在上传 %{smart_count} 个文件',
|
||||
xFilesSelected: '%{smart_count} 个文件待上传',
|
||||
xMoreFilesAdded: '又有 %{smart_count} 个文件被添加',
|
||||
uploadPaused: '上传已暂停',
|
||||
uploadStalled: '上传已 %{seconds} 秒没有进度。您可能需要重试。',
|
||||
uploadXFiles: '上传 %{smart_count} 个文件',
|
||||
uploadXNewFiles: '上传 +%{smart_count} 个文件',
|
||||
xFilesSelected: '已选择 %{smart_count} 个文件',
|
||||
xMoreFilesAdded: '又添加了 %{smart_count} 个文件',
|
||||
xTimeLeft: '剩余 %{time}',
|
||||
youCanOnlyUploadFileTypes: '您只能上传这些文件类型:%{types}',
|
||||
youCanOnlyUploadFileTypes: '您只能上传:%{types}',
|
||||
youCanOnlyUploadX: '您只能上传 %{smart_count} 个文件',
|
||||
youHaveToAtLeastSelectX: '您至少要选择 %{smart_count} 个文件',
|
||||
zoomIn: '放大',
|
||||
zoomOut: '缩小',
|
||||
}
|
||||
|
||||
// @ts-ignore untyped
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ zh_TW.strings = {
|
|||
additionalRestrictionsFailed: '尚未滿足 %{count} 個額外限制',
|
||||
addMore: '新增更多',
|
||||
addMoreFiles: '新增更多檔案',
|
||||
aggregateExceedsSize:
|
||||
'您選擇了 %{size} 的檔案,但最大允許大小為 %{sizeAllowed}',
|
||||
allFilesFromFolderNamed: '資料夾 %{name} 中的所有檔案',
|
||||
allowAccessDescription: '請允許本網站存取相機以拍照或錄影。',
|
||||
allowAccessTitle: '請允許對相機的存取權限',
|
||||
|
|
@ -23,6 +25,7 @@ zh_TW.strings = {
|
|||
aspectRatioPortrait: '裁切成直向 (9:16)',
|
||||
aspectRatioSquare: '裁切成方形',
|
||||
authAborted: '驗證已中止',
|
||||
authenticate: '連接',
|
||||
authenticateWith: '連接到 %{pluginName}',
|
||||
authenticateWithTitle: '請使用 %{pluginName} 進行身份驗證以選擇檔案',
|
||||
back: '返回',
|
||||
|
|
@ -46,8 +49,11 @@ zh_TW.strings = {
|
|||
dashboardTitle: 'Uppy 儀表板',
|
||||
dashboardWindowTitle: 'Uppy 儀表板視窗(按下 Esc 以關閉)',
|
||||
dataUploadedOfTotal: '%{complete}/%{total}',
|
||||
dataUploadedOfUnknown: '%{complete} / 未知大小',
|
||||
discardMediaFile: '捨棄媒體檔案',
|
||||
discardRecordedFile: '捨棄錄影檔',
|
||||
done: '完成',
|
||||
dropHereOr: '將檔案拖曳到這裡或者%{browse}',
|
||||
dropHint: '將檔案拖曳至此處',
|
||||
dropPasteBoth: '將檔案拖曳至此處、%{browseFiles},或%{browseFolders}',
|
||||
dropPasteFiles: '將檔案拖曳至此處,或%{browseFiles}',
|
||||
|
|
@ -79,7 +85,9 @@ zh_TW.strings = {
|
|||
importFiles: '檔案匯入來源:',
|
||||
importFrom: '從 %{name} 匯入',
|
||||
inferiorSize: '此檔案小於允許的大小:%{size}',
|
||||
loadedXFiles: '已載入 %{numFiles} 個檔案',
|
||||
loading: '正在載入...',
|
||||
logIn: '登入',
|
||||
logOut: '登出',
|
||||
micDisabled: '使用者拒絕麥克風存取權限',
|
||||
missingRequiredMetaField: '遺漏中繼資料必要欄位',
|
||||
|
|
@ -99,15 +107,14 @@ zh_TW.strings = {
|
|||
pause: '暫停',
|
||||
paused: '已暫停',
|
||||
pauseUpload: '暫停上傳',
|
||||
pickFiles: '選擇檔案',
|
||||
pickPhotos: '選擇照片',
|
||||
pleaseWait: '請稍候',
|
||||
pluginNameAudio: '音訊',
|
||||
pluginNameBox: 'Box',
|
||||
pluginNameCamera: '相機',
|
||||
pluginNameDropbox: 'Dropbox',
|
||||
pluginNameFacebook: 'Facebook',
|
||||
pluginNameGoogleDrive: 'Google 雲端硬碟',
|
||||
pluginNameInstagram: 'Instagram',
|
||||
pluginNameOneDrive: 'OneDrive',
|
||||
pluginNameZoom: 'Zoom',
|
||||
pluginNameScreenCapture: '螢幕錄製',
|
||||
pluginNameUrl: '連結',
|
||||
pluginWebdavInputLabel: 'WebDAV 檔案 URL(例如來自 ownCloud 或 Nextcloud)',
|
||||
poweredBy: '技術提供者 %{uppy}',
|
||||
processingXFiles: '正在處理 %{smart_count} 個檔案',
|
||||
recording: '正在錄製',
|
||||
|
|
@ -147,7 +154,9 @@ zh_TW.strings = {
|
|||
submitRecordedFile: '提交錄影檔',
|
||||
takePicture: '拍照',
|
||||
takePictureBtn: '拍照',
|
||||
takeScreenshot: '螢幕截圖',
|
||||
timedOut: '上傳已停滯 %{seconds} 秒,正在中止上傳。',
|
||||
unnamed: '未命名',
|
||||
upload: '上傳',
|
||||
uploadComplete: '上傳完成',
|
||||
uploadFailed: '上傳失敗',
|
||||
|
|
|
|||
|
|
@ -122,3 +122,9 @@ export default class OneDrive<M extends Meta, B extends Body>
|
|||
return this.view.render(state)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
OneDrive: OneDrive<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,10 @@ type RenderOpts<M extends Meta, B extends Body> = Omit<
|
|||
export default class ProviderView<M extends Meta, B extends Body> {
|
||||
static VERSION = packageJson.version
|
||||
|
||||
// Test hook (mirrors GoldenRetriever pattern): allow tests to override debounce time
|
||||
// @ts-expect-error test-only hook key
|
||||
static [Symbol.for('uppy test: searchDebounceMs')]: number | undefined
|
||||
|
||||
plugin: UnknownProviderPlugin<M, B>
|
||||
|
||||
provider: UnknownProviderPlugin<M, B>['provider']
|
||||
|
|
@ -119,6 +123,7 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
isHandlingScroll: boolean = false
|
||||
|
||||
previousCheckbox: string | null = null
|
||||
#searchDebounced: () => void
|
||||
|
||||
constructor(plugin: UnknownProviderPlugin<M, B>, opts: PassedOpts<M, B>) {
|
||||
this.plugin = plugin
|
||||
|
|
@ -157,6 +162,16 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
this.provider.provider,
|
||||
this.provider,
|
||||
)
|
||||
|
||||
// Configure debounced search with test override
|
||||
const testHookSymbol = Symbol.for('uppy test: searchDebounceMs')
|
||||
const testWait = (
|
||||
ProviderView as unknown as Record<symbol, number | undefined>
|
||||
)[testHookSymbol]
|
||||
const wait = testWait ?? 500
|
||||
const debounceOpts =
|
||||
testWait === 0 ? { leading: true, trailing: true } : undefined
|
||||
this.#searchDebounced = debounce(this.#search, wait, debounceOpts)
|
||||
}
|
||||
|
||||
resetPluginState(): void {
|
||||
|
|
@ -333,11 +348,13 @@ export default class ProviderView<M extends Meta, B extends Body> {
|
|||
this.setLoading(false)
|
||||
}
|
||||
|
||||
#searchDebounced = debounce(this.#search, 500)
|
||||
// debounced search function is initialized in the constructor
|
||||
|
||||
onSearchInput = (s: string): void => {
|
||||
this.plugin.setPluginState({ searchString: s })
|
||||
if (this.opts.supportsSearch) this.#searchDebounced()
|
||||
if (this.opts.supportsSearch) {
|
||||
this.#searchDebounced()
|
||||
}
|
||||
}
|
||||
|
||||
async openSearchResultFolder(folderId: PartialTreeId): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class DashboardModal<M extends Meta, B extends Body> extends Component<
|
|||
|
||||
uppy.use(DashboardPlugin<M, B>, options)
|
||||
|
||||
this.plugin = uppy.getPlugin(options.id) as DashboardPlugin<M, B>
|
||||
this.plugin = uppy.getPlugin(options.id)!
|
||||
if (open) {
|
||||
this.plugin.openModal()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,3 +122,9 @@ export default class RemoteSources<
|
|||
this.#installedPlugins.clear()
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
RemoteSources: RemoteSources<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ import locale from './locale.js'
|
|||
import RecorderScreen from './RecorderScreen.js'
|
||||
import ScreenRecIcon from './ScreenRecIcon.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
ScreenCapture: ScreenCapture<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
// Check if screen capturing is supported.
|
||||
// mediaDevices is supprted on mobile Safari, getDisplayMedia is not
|
||||
function isScreenRecordingSupported() {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ import type { StatusBarOptions } from './StatusBarOptions.js'
|
|||
import statusBarStates from './StatusBarStates.js'
|
||||
import StatusBarUI, { type StatusBarUIProps } from './StatusBarUI.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
StatusBar: StatusBar<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
const speedFilterHalfLife = 2000
|
||||
const ETAFilterHalfLife = 2000
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const installPlugin = () => {
|
|||
} satisfies DashboardOptions<M, B>;
|
||||
|
||||
uppy.use(DashboardPlugin<M, B>, options);
|
||||
plugin = uppy.getPlugin(options.id) as DashboardPlugin<M, B>;
|
||||
plugin = uppy.getPlugin(options.id)!;
|
||||
};
|
||||
const uninstallPlugin = (uppyInstance: Uppy<M, B> = uppy) => {
|
||||
if (plugin != null) uppyInstance.removePlugin(plugin);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const installPlugin = () => {
|
|||
} satisfies DashboardOptions<M, B>;
|
||||
|
||||
uppy.use(DashboardPlugin<M, B>, options);
|
||||
plugin = uppy.getPlugin(options.id) as DashboardPlugin<M, B>;
|
||||
plugin = uppy.getPlugin(options.id)!;
|
||||
if (open) plugin.openModal();
|
||||
};
|
||||
const uninstallPlugin = (uppyInstance: Uppy<M, B> = uppy) => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const installPlugin = () => {
|
|||
} satisfies StatusBarOptions;
|
||||
|
||||
uppy.use(StatusBarPlugin<M, B>, options);
|
||||
plugin = uppy.getPlugin(options.id) as StatusBarPlugin<M, B>;
|
||||
plugin = uppy.getPlugin(options.id)!;
|
||||
};
|
||||
const uninstallPlugin = (uppyInstance: Uppy<M, B> = uppy) => {
|
||||
if (plugin != null) uppyInstance.removePlugin(plugin);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ declare module '@uppy/core' {
|
|||
'thumbnail:request': (file: UppyFile<M, B>) => void
|
||||
'thumbnail:cancel': (file: UppyFile<M, B>) => void
|
||||
}
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
ThumbnailGenerator: ThumbnailGenerator<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
interface Rotation {
|
||||
|
|
@ -500,3 +503,9 @@ export default class ThumbnailGenerator<
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
ThumbnailGenerator: ThumbnailGenerator<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ declare module '@uppy/core' {
|
|||
progress_combined?: number
|
||||
}) => void
|
||||
}
|
||||
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Transloadit: Transloadit<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/utils' {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
import { BasePlugin, EventManager } from '@uppy/core'
|
||||
import {
|
||||
filterFilesToEmitUploadStarted,
|
||||
filterNonFailedFiles,
|
||||
filterFilesToUpload,
|
||||
getAllowedMetaFields,
|
||||
hasProperty,
|
||||
isNetworkError,
|
||||
|
|
@ -106,6 +106,12 @@ declare module '@uppy/utils' {
|
|||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Tus: Tus<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tus resumable file uploader
|
||||
*/
|
||||
|
|
@ -557,7 +563,7 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
}
|
||||
|
||||
async #uploadFiles(files: UppyFile<M, B>[]) {
|
||||
const filesFiltered = filterNonFailedFiles(files)
|
||||
const filesFiltered = filterFilesToUpload(files)
|
||||
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
|
||||
this.uppy.emit('upload-start', filesToEmit)
|
||||
|
||||
|
|
|
|||
|
|
@ -112,3 +112,9 @@ export default class Unsplash<M extends Meta, B extends Body>
|
|||
this.unmount()
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Unsplash: Unsplash<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ import locale from './locale.js'
|
|||
import UrlUI from './UrlUI.js'
|
||||
import forEachDroppedOrPastedUrl from './utils/forEachDroppedOrPastedUrl.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Url: Url<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
function UrlIcon() {
|
||||
return (
|
||||
<svg
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import type { UppyFile } from './UppyFile.js'
|
||||
|
||||
export function filterNonFailedFiles(
|
||||
const hasError = (file: UppyFile<any, any>) => 'error' in file && !!file.error
|
||||
|
||||
// We don't need to re-upload already successfully uploaded files
|
||||
// so let's exclude them here:
|
||||
// https://github.com/transloadit/uppy/issues/5930
|
||||
// This happens for example when restoring a partially finished session (e.g. using golden retriever).
|
||||
const isCompleted = (file: UppyFile<any, any>) => file.progress.uploadComplete
|
||||
|
||||
export function filterFilesToUpload(
|
||||
files: UppyFile<any, any>[],
|
||||
): UppyFile<any, any>[] {
|
||||
const hasError = (file: UppyFile<any, any>): boolean =>
|
||||
'error' in file && !!file.error
|
||||
|
||||
return files.filter((file) => !hasError(file))
|
||||
return files.filter((file) => !hasError(file) && !isCompleted(file))
|
||||
}
|
||||
|
||||
// Don't double-emit upload-started for Golden Retriever-restored files that were already started
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export { default as fetchWithNetworkError } from './fetchWithNetworkError.js'
|
|||
|
||||
export {
|
||||
filterFilesToEmitUploadStarted,
|
||||
filterNonFailedFiles,
|
||||
filterFilesToUpload,
|
||||
} from './fileFilters.js'
|
||||
|
||||
export { default as findAllDOMElements } from './findAllDOMElements.js'
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default defineComponent({
|
|||
target: containerRef.value,
|
||||
}
|
||||
uppy.use(DashboardPlugin, options)
|
||||
pluginRef.value = uppy.getPlugin(options.id) as DashboardPlugin<any, any>
|
||||
pluginRef.value = uppy.getPlugin(options.id)!
|
||||
}
|
||||
|
||||
useUppy(onMount, pluginRef, props.uppy, propsRef)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export default defineComponent({
|
|||
target: containerRef.value,
|
||||
}
|
||||
uppy.use(DashboardPlugin, options)
|
||||
pluginRef.value = uppy.getPlugin(options.id) as DashboardPlugin<any, any>
|
||||
pluginRef.value = uppy.getPlugin(options.id)!
|
||||
}
|
||||
|
||||
useUppy(onMount, pluginRef, props.uppy, propsRef)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default defineComponent({
|
|||
target: containerRef.value,
|
||||
}
|
||||
uppy.use(StatusBarPlugin, options)
|
||||
pluginRef.value = uppy.getPlugin(options.id) as StatusBarPlugin<any, any>
|
||||
pluginRef.value = uppy.getPlugin(options.id)!
|
||||
}
|
||||
|
||||
useUppy(onMount, pluginRef, props.uppy, propsRef)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import Uppy from '@uppy/core'
|
||||
import { Uppy } from '@uppy/core'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import Webcam from './index.js'
|
||||
|
||||
|
|
@ -10,10 +10,9 @@ describe('Webcam', () => {
|
|||
isTypeSupported: () => true,
|
||||
}
|
||||
|
||||
const uppy = new Uppy<any, any>().use(Webcam)
|
||||
const uppy = new Uppy().use(Webcam)
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).not.toBeDefined()
|
||||
})
|
||||
|
||||
|
|
@ -27,8 +26,7 @@ describe('Webcam', () => {
|
|||
preferredVideoMimeType: 'video/webm',
|
||||
})
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).toEqual('video/webm')
|
||||
})
|
||||
|
||||
|
|
@ -42,8 +40,7 @@ describe('Webcam', () => {
|
|||
preferredVideoMimeType: 'video/mp4',
|
||||
})
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).not.toBeDefined()
|
||||
})
|
||||
|
||||
|
|
@ -57,8 +54,7 @@ describe('Webcam', () => {
|
|||
restrictions: { allowedFileTypes: ['video/mp4', 'video/webm'] },
|
||||
}).use(Webcam)
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).toEqual('video/mp4')
|
||||
})
|
||||
|
||||
|
|
@ -72,8 +68,7 @@ describe('Webcam', () => {
|
|||
restrictions: { allowedFileTypes: ['video/mp4', 'video/webm'] },
|
||||
}).use(Webcam)
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).toEqual('video/webm')
|
||||
})
|
||||
|
||||
|
|
@ -89,8 +84,7 @@ describe('Webcam', () => {
|
|||
preferredVideoMimeType: 'video/webm',
|
||||
})
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).toEqual('video/webm')
|
||||
})
|
||||
|
||||
|
|
@ -104,8 +98,7 @@ describe('Webcam', () => {
|
|||
restrictions: { allowedFileTypes: ['video/mp4', 'video/webm'] },
|
||||
}).use(Webcam)
|
||||
expect(
|
||||
(uppy.getPlugin('Webcam') as Webcam<any, any>).getMediaRecorderOptions()
|
||||
.mimeType,
|
||||
uppy.getPlugin('Webcam')?.getMediaRecorderOptions().mimeType,
|
||||
).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ import locale from './locale.js'
|
|||
import PermissionsScreen from './PermissionsScreen.js'
|
||||
import supportsMediaRecorder from './supportsMediaRecorder.js'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Webcam: Webcam<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a MIME type or file extension into a MIME type.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -160,3 +160,9 @@ export default class Webdav<M extends Meta, B extends Body>
|
|||
return this.view.render(state)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
WebDav: Webdav<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
type FetcherOptions,
|
||||
fetcher,
|
||||
filterFilesToEmitUploadStarted,
|
||||
filterNonFailedFiles,
|
||||
filterFilesToUpload,
|
||||
getAllowedMetaFields,
|
||||
internalRateLimitedQueue,
|
||||
isNetworkError,
|
||||
|
|
@ -86,6 +86,12 @@ declare module '@uppy/core' {
|
|||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
XHRUpload: XHRUpload<M, B>
|
||||
}
|
||||
}
|
||||
|
||||
function buildResponseError(
|
||||
xhr?: XMLHttpRequest,
|
||||
err?: string | Error | NetworkError,
|
||||
|
|
@ -529,7 +535,7 @@ export default class XHRUpload<
|
|||
this.uppy.log('[XHRUpload] Uploading...')
|
||||
const files = this.uppy.getFilesByIds(fileIDs)
|
||||
|
||||
const filesFiltered = filterNonFailedFiles(files)
|
||||
const filesFiltered = filterFilesToUpload(files)
|
||||
const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
|
||||
this.uppy.emit('upload-start', filesToEmit)
|
||||
|
||||
|
|
|
|||
|
|
@ -107,3 +107,9 @@ export default class Zoom<M extends Meta, B extends Body>
|
|||
return this.view.render(state)
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
|
||||
Zoom: Zoom<M, B>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10907,6 +10907,7 @@ __metadata:
|
|||
dependencies:
|
||||
"@transloadit/prettier-bytes": "npm:^0.3.4"
|
||||
"@uppy/core": "workspace:^"
|
||||
"@uppy/dropbox": "workspace:^"
|
||||
"@uppy/google-drive": "workspace:^"
|
||||
"@uppy/provider-views": "workspace:^"
|
||||
"@uppy/thumbnail-generator": "workspace:^"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue