mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
Merge branch 'main' into restore/pr-6224
This commit is contained in:
commit
0a423a45b9
409 changed files with 14096 additions and 11418 deletions
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
"@uppy/xhr-upload": minor
|
|
||||||
"@uppy/utils": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Introduce modern, minimal TaskQueue to replace RateLimitedQueue
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
"@uppy/transloadit": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Add type re-export for `AssemblyInstructionsInput`
|
|
||||||
45
.changeset/curvy-feet-chew.md
Normal file
45
.changeset/curvy-feet-chew.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
"@uppy/angular": patch
|
||||||
|
"@uppy/google-photos-picker": patch
|
||||||
|
"@uppy/google-drive-picker": patch
|
||||||
|
"@uppy/thumbnail-generator": patch
|
||||||
|
"@uppy/companion-client": patch
|
||||||
|
"@uppy/golden-retriever": patch
|
||||||
|
"@uppy/image-generator": patch
|
||||||
|
"@uppy/provider-views": patch
|
||||||
|
"@uppy/remote-sources": patch
|
||||||
|
"@uppy/screen-capture": patch
|
||||||
|
"@uppy/store-default": patch
|
||||||
|
"@uppy/google-drive": patch
|
||||||
|
"@uppy/image-editor": patch
|
||||||
|
"@uppy/transloadit": patch
|
||||||
|
"@uppy/components": patch
|
||||||
|
"@uppy/compressor": patch
|
||||||
|
"@uppy/status-bar": patch
|
||||||
|
"@uppy/xhr-upload": patch
|
||||||
|
"@uppy/companion": patch
|
||||||
|
"@uppy/dashboard": patch
|
||||||
|
"@uppy/drag-drop": patch
|
||||||
|
"@uppy/facebook": patch
|
||||||
|
"@uppy/onedrive": patch
|
||||||
|
"@uppy/unsplash": patch
|
||||||
|
"@uppy/dropbox": patch
|
||||||
|
"@uppy/locales": patch
|
||||||
|
"@uppy/aws-s3": patch
|
||||||
|
"@uppy/svelte": patch
|
||||||
|
"@uppy/webcam": patch
|
||||||
|
"@uppy/webdav": patch
|
||||||
|
"@uppy/audio": patch
|
||||||
|
"@uppy/react": patch
|
||||||
|
"@uppy/utils": patch
|
||||||
|
"@uppy/core": patch
|
||||||
|
"@uppy/form": patch
|
||||||
|
"@uppy/zoom": patch
|
||||||
|
"@uppy/box": patch
|
||||||
|
"@uppy/tus": patch
|
||||||
|
"@uppy/url": patch
|
||||||
|
"@uppy/vue": patch
|
||||||
|
"uppy": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Bump shared runtime dependencies (preact, nanoid, lodash, classnames, shallow-equal, pretty-bytes, p-queue, tus-js-client, @transloadit/types @transloadit/prettier-bytes v1, is-mobile, exifr, compressorjs, rxjs, tslib). Also includes type-only fixes in `@uppy/companion`'s `jwt.ts` and `request.ts` to track `@types/jsonwebtoken` v9 and `@types/node`.
|
||||||
5
.changeset/few-plants-pump.md
Normal file
5
.changeset/few-plants-pump.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix "My Device" button in dashboard, it now respects the fileManagerSelectionType.
|
||||||
9
.changeset/gold-rice-jog.md
Normal file
9
.changeset/gold-rice-jog.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
"@uppy/components": major
|
||||||
|
"@uppy/companion": major
|
||||||
|
"uppy": major
|
||||||
|
"@uppy/transloadit": minor
|
||||||
|
"@uppy/locales": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove @uppy/instagram references from all the packages
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
"@uppy/companion": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix bug with 429 not returning JSON response with message
|
|
||||||
5
.changeset/happy-timers-kick.md
Normal file
5
.changeset/happy-timers-kick.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/aws-s3": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve type
|
||||||
5
.changeset/lazy-berries-brake.md
Normal file
5
.changeset/lazy-berries-brake.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/companion-client": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
uploadRemoteFile() now queues token request and websocket request as a single job in the request queue.
|
||||||
5
.changeset/legal-drinks-marry.md
Normal file
5
.changeset/legal-drinks-marry.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/transloadit": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add assemblyStatus and lastAssemblyStatus to transloadit's plugin state
|
||||||
8
.changeset/mighty-deers-sin.md
Normal file
8
.changeset/mighty-deers-sin.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
"@uppy/companion-client": major
|
||||||
|
"@uppy/companion": major
|
||||||
|
---
|
||||||
|
|
||||||
|
Send token using websocket instead of window.opener.
|
||||||
|
Breaking in `@uppy/companion-client` because it needs newest version of Companion in order to work.
|
||||||
|
Breaking in `@uppy/companion` because `companion.socket()` now requires `companionOptions` to be passed as the second argument.
|
||||||
5
.changeset/proud-spies-train.md
Normal file
5
.changeset/proud-spies-train.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/transloadit": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
emit updated AssemblyState in 'transloadit:assembly-cancelled' event
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
"@uppy/companion": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
upgrade @aws-sdk/ deps in @uppy/companion
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
"@uppy/transloadit": patch
|
|
||||||
"uppy": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
remove monkey patch from uppy bundle package
|
|
||||||
5
.changeset/smooth-chefs-occur.md
Normal file
5
.changeset/smooth-chefs-occur.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/companion": major
|
||||||
|
---
|
||||||
|
|
||||||
|
Upgrade to express 5 - Companion no longer works when used as a middleware with Express 4.
|
||||||
5
.changeset/three-cobras-lick.md
Normal file
5
.changeset/three-cobras-lick.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@uppy/companion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Port Companion to TypeScript. Not really a breaking change but there could be some unexpected breakage.
|
||||||
|
|
@ -44,9 +44,6 @@ COMPANION_DROPBOX_SECRET=***
|
||||||
COMPANION_GOOGLE_KEY=***
|
COMPANION_GOOGLE_KEY=***
|
||||||
COMPANION_GOOGLE_SECRET=***
|
COMPANION_GOOGLE_SECRET=***
|
||||||
|
|
||||||
COMPANION_INSTAGRAM_KEY=***
|
|
||||||
COMPANION_INSTAGRAM_SECRET=***
|
|
||||||
|
|
||||||
COMPANION_FACEBOOK_KEY=***
|
COMPANION_FACEBOOK_KEY=***
|
||||||
COMPANION_FACEBOOK_SECRET=***
|
COMPANION_FACEBOOK_SECRET=***
|
||||||
|
|
||||||
|
|
|
||||||
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
|
@ -57,7 +57,7 @@ This is not needed for changing existing components.
|
||||||
## Companion
|
## Companion
|
||||||
|
|
||||||
If you’d like to work on features that the basic development version of Uppy
|
If you’d like to work on features that the basic development version of Uppy
|
||||||
doesn’t support, such as Uppy integrations with Instagram/Google Drive/Facebook
|
doesn’t support, such as Uppy integrations with Google Drive/Facebook
|
||||||
etc., you need to set up your `.env` file (copy the contents of `.env.example`
|
etc., you need to set up your `.env` file (copy the contents of `.env.example`
|
||||||
and adjust them based on what you need to work on), and run:
|
and adjust them based on what you need to work on), and run:
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ when files are changed.
|
||||||
### How the Authentication and Token mechanism works
|
### How the Authentication and Token mechanism works
|
||||||
|
|
||||||
This section describes how Authentication works between Companion and Providers.
|
This section describes how Authentication works between Companion and Providers.
|
||||||
While this behaviour is the same for all Providers (Dropbox, Instagram, Google
|
While this behaviour is the same for all Providers (Dropbox, Google
|
||||||
Drive, etc.), we are going to be referring to Dropbox in place of any Provider
|
Drive, etc.), we are going to be referring to Dropbox in place of any Provider
|
||||||
throughout this section.
|
throughout this section.
|
||||||
|
|
||||||
|
|
|
||||||
2
.github/workflows/bundlers.yml
vendored
2
.github/workflows/bundlers.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run:
|
run:
|
||||||
corepack yarn install
|
corepack yarn install --immutable
|
||||||
env:
|
env:
|
||||||
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
|
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
|
||||||
CYPRESS_INSTALL_BINARY: 0
|
CYPRESS_INSTALL_BINARY: 0
|
||||||
|
|
|
||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -47,7 +47,7 @@ jobs:
|
||||||
node-version: ${{matrix.node-version}}
|
node-version: ${{matrix.node-version}}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run:
|
run:
|
||||||
corepack yarn install
|
corepack yarn install --immutable
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: corepack yarn workspace @uppy/dashboard playwright install --with-deps
|
run: corepack yarn workspace @uppy/dashboard playwright install --with-deps
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
@ -84,7 +84,7 @@ jobs:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run:
|
run:
|
||||||
corepack yarn install
|
corepack yarn install --immutable
|
||||||
- run: corepack yarn run typecheck
|
- run: corepack yarn run typecheck
|
||||||
|
|
||||||
lint_js:
|
lint_js:
|
||||||
|
|
|
||||||
67
.github/workflows/companion-deploy.yml
vendored
67
.github/workflows/companion-deploy.yml
vendored
|
|
@ -38,56 +38,49 @@ jobs:
|
||||||
path: /tmp/companion-${{ github.sha }}.tar.gz
|
path: /tmp/companion-${{ github.sha }}.tar.gz
|
||||||
|
|
||||||
docker:
|
docker:
|
||||||
name: DockerHub
|
name: DockerHub (${{ matrix.platform }})
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
env:
|
matrix:
|
||||||
DOCKER_BUILDKIT: 0
|
include:
|
||||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
- runner: ubuntu-24.04
|
||||||
|
platform: linux/amd64
|
||||||
|
platform_tag: amd64
|
||||||
|
- runner: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64
|
||||||
|
platform_tag: arm64
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Docker meta
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||||
id: docker_meta
|
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
|
||||||
with:
|
|
||||||
images: transloadit/companion
|
|
||||||
tags: |
|
|
||||||
type=edge
|
|
||||||
type=raw,value=latest,enable=false
|
|
||||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Log in to DockerHub
|
- name: Log in to DockerHub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
username: ${{secrets.DOCKER_USERNAME}}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{secrets.DOCKER_PASSWORD}}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: transloadit/companion:edge-${{ matrix.platform_tag }}
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
|
||||||
|
|
||||||
heroku:
|
docker-manifest:
|
||||||
name: Heroku
|
name: DockerHub manifest
|
||||||
|
needs: docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||||
uses: actions/checkout@v6
|
- name: Log in to DockerHub
|
||||||
- name: Alter dockerfile
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
run: |
|
|
||||||
sed -i 's/^EXPOSE 3020$/EXPOSE $PORT/g' Dockerfile
|
|
||||||
# https://github.com/AkhileshNS/heroku-deploy/issues/188
|
|
||||||
- name: Install Heroku CLI
|
|
||||||
run: |
|
|
||||||
curl https://cli-assets.heroku.com/install.sh | sh
|
|
||||||
- name: Deploy to heroku
|
|
||||||
uses: akhileshns/heroku-deploy@e3eb99d45a8e2ec5dca08735e089607befa4bf28 # v3.14.15
|
|
||||||
with:
|
with:
|
||||||
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
heroku_app_name: companion-demo
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
heroku_email: ${{secrets.HEROKU_EMAIL}}
|
- name: Publish edge manifest
|
||||||
usedocker: true
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag transloadit/companion:edge \
|
||||||
|
transloadit/companion:edge-amd64 \
|
||||||
|
transloadit/companion:edge-arm64
|
||||||
|
|
|
||||||
12
.github/workflows/manual-cdn.yml
vendored
12
.github/workflows/manual-cdn.yml
vendored
|
|
@ -30,18 +30,6 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run:
|
|
||||||
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- uses: actions/cache@v5
|
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
||||||
|
|
||||||
- name: Create Release Pull Request or Publish
|
- name: Create Release Pull Request or Publish
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d
|
||||||
with:
|
with:
|
||||||
version: corepack yarn run version
|
version: corepack yarn run version
|
||||||
publish: corepack yarn run release
|
publish: corepack yarn run release
|
||||||
|
|
@ -102,7 +102,7 @@ jobs:
|
||||||
# set latest tag for default branch
|
# set latest tag for default branch
|
||||||
type=raw,value=latest,enable=true
|
type=raw,value=latest,enable=true
|
||||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||||
- name: Log in to DockerHub
|
- name: Log in to DockerHub
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@ initScope: uppy
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
npmPublishAccess: public
|
npmMinimalAgeGate: 2880
|
||||||
|
|
||||||
|
npmPublishAccess: public
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can
|
Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can
|
||||||
use this from a CDN
|
use this from a CDN
|
||||||
(`<script src="https://releases.transloadit.com/uppy/v5.2.3/uppy.min.js"></script>`)
|
(`<script src="https://releases.transloadit.com/uppy/v5.2.4/uppy.min.js"></script>`)
|
||||||
or bundle it with your webapp.
|
or bundle it with your webapp.
|
||||||
|
|
||||||
Note that the recommended way to use Uppy is to install it with yarn/npm and use
|
Note that the recommended way to use Uppy is to install it with yarn/npm and use
|
||||||
|
|
@ -83,7 +83,7 @@ inline into the page. This, and many more configuration options can be found
|
||||||
here: <https://uppy.io/docs/dashboard/>.
|
here: <https://uppy.io/docs/dashboard/>.
|
||||||
|
|
||||||
Uppy has many more Plugins besides Xhr and the Dashboard. For example, you can
|
Uppy has many more Plugins besides Xhr and the Dashboard. For example, you can
|
||||||
enable Webcam, Instagram, or video encoding support. Note that for some Plugins,
|
enable Webcam or video encoding support. Note that for some Plugins,
|
||||||
you will need to run a server side component called: Companion. Those plugins
|
you will need to run a server side component called: Companion. Those plugins
|
||||||
are marked with a (c) symbol. Alternatively, you can sign up for a free
|
are marked with a (c) symbol. Alternatively, you can sign up for a free
|
||||||
Transloadit account. Transloadit runs Companion for you, tusd servers to handle
|
Transloadit account. Transloadit runs Companion for you, tusd servers to handle
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ yarn workspace @uppy/[package-name] build
|
||||||
- **Uppy Core** (`@uppy/core`) - Main class that manages plugins, state, and events
|
- **Uppy Core** (`@uppy/core`) - Main class that manages plugins, state, and events
|
||||||
- **Plugins** - Modular components for different functionalities:
|
- **Plugins** - Modular components for different functionalities:
|
||||||
- **UI Plugins**: Dashboard, Drag & Drop, File Input, Webcam, etc.
|
- **UI Plugins**: Dashboard, Drag & Drop, File Input, Webcam, etc.
|
||||||
- **Provider Plugins**: Google Drive, Dropbox, Instagram, etc. (require Companion)
|
- **Provider Plugins**: Google Drive, Dropbox, etc. (require Companion)
|
||||||
- **Uploader Plugins**: Tus (resumable), XHR Upload, AWS S3, etc.
|
- **Uploader Plugins**: Tus (resumable), XHR Upload, AWS S3, etc.
|
||||||
- **Utility Plugins**: Golden Retriever (recovery), Thumbnail Generator, etc.
|
- **Utility Plugins**: Golden Retriever (recovery), Thumbnail Generator, etc.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,12 @@ FROM node:22.22.1-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# copy required files from build stage.
|
# copy required files from build stage.
|
||||||
COPY --from=build /app/packages/@uppy/companion/bin /app/bin
|
COPY --from=build /app/packages/@uppy/companion/dist /app/dist
|
||||||
COPY --from=build /app/packages/@uppy/companion/lib /app/lib
|
|
||||||
COPY --from=build /app/packages/@uppy/companion/package.json /app/package.json
|
COPY --from=build /app/packages/@uppy/companion/package.json /app/package.json
|
||||||
COPY --from=build /app/packages/@uppy/companion/node_modules /app/node_modules
|
COPY --from=build /app/packages/@uppy/companion/node_modules /app/node_modules
|
||||||
|
|
||||||
ENV PATH "${PATH}:/app/node_modules/.bin"
|
ENV PATH "${PATH}:/app/node_modules/.bin"
|
||||||
|
|
||||||
CMD ["node","/app/bin/companion"]
|
CMD ["node","/app/dist/bin/companion.js"]
|
||||||
# This can be overruled later
|
# This can be overruled later
|
||||||
EXPOSE 3020
|
EXPOSE 3020
|
||||||
|
|
|
||||||
20
README.md
20
README.md
|
|
@ -7,7 +7,7 @@ with any application. It’s fast, has a comprehensible API and lets you worry
|
||||||
about more important problems than building a file uploader.
|
about more important problems than building a file uploader.
|
||||||
|
|
||||||
- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Box,
|
- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Box,
|
||||||
Instagram or snap and record selfies with a camera
|
or snap and record selfies with a camera
|
||||||
- **Preview** and edit metadata with a nice interface
|
- **Preview** and edit metadata with a nice interface
|
||||||
- **Upload** to the final destination, optionally process/encode
|
- **Upload** to the final destination, optionally process/encode
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ For the supported frameworks (except Angular) Uppy offers three ways to build us
|
||||||
- Lightweight, modular plugin-based architecture, light on dependencies :zap:
|
- Lightweight, modular plugin-based architecture, light on dependencies :zap:
|
||||||
- Resumable file uploads via the open [tus](https://tus.io/) standard, so large
|
- Resumable file uploads via the open [tus](https://tus.io/) standard, so large
|
||||||
uploads survive network hiccups
|
uploads survive network hiccups
|
||||||
- Supports picking files from: Webcam, Dropbox, Box, Google Drive, Instagram,
|
- Supports picking files from: Webcam, Dropbox, Box, Google Drive,
|
||||||
bypassing the user’s device where possible, syncing between servers directly
|
bypassing the user’s device where possible, syncing between servers directly
|
||||||
via [@uppy/companion](https://uppy.io/docs/companion)
|
via [@uppy/companion](https://uppy.io/docs/companion)
|
||||||
- Works great with file encoding and processing backends, such as
|
- Works great with file encoding and processing backends, such as
|
||||||
|
|
@ -103,7 +103,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus
|
||||||
```
|
```
|
||||||
|
|
||||||
Add CSS
|
Add CSS
|
||||||
[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.3/uppy.min.css),
|
[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.4/uppy.min.css),
|
||||||
either to your HTML page’s `<head>` or include in JS, if your bundler of choice
|
either to your HTML page’s `<head>` or include in JS, if your bundler of choice
|
||||||
supports it.
|
supports it.
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||||
```html
|
```html
|
||||||
<!-- 1. Add CSS to `<head>` -->
|
<!-- 1. Add CSS to `<head>` -->
|
||||||
<link
|
<link
|
||||||
href="https://releases.transloadit.com/uppy/v5.2.3/uppy.min.css"
|
href="https://releases.transloadit.com/uppy/v5.2.4/uppy.min.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -128,7 +128,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||||
Uppy,
|
Uppy,
|
||||||
Dashboard,
|
Dashboard,
|
||||||
Tus,
|
Tus,
|
||||||
} from 'https://releases.transloadit.com/uppy/v5.2.3/uppy.min.mjs'
|
} from 'https://releases.transloadit.com/uppy/v5.2.4/uppy.min.mjs'
|
||||||
|
|
||||||
const uppy = new Uppy()
|
const uppy = new Uppy()
|
||||||
uppy.use(Dashboard, { target: '#files-drag-drop' })
|
uppy.use(Dashboard, { target: '#files-drag-drop' })
|
||||||
|
|
@ -140,7 +140,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||||
|
|
||||||
- [Uppy](https://uppy.io/docs/uppy/) — full list of options, methods and events
|
- [Uppy](https://uppy.io/docs/uppy/) — full list of options, methods and events
|
||||||
- [Companion](https://uppy.io/docs/companion/) — setting up and running a
|
- [Companion](https://uppy.io/docs/companion/) — setting up and running a
|
||||||
Companion instance, which adds support for Instagram, Dropbox, Box, Google
|
Companion instance, which adds support for Dropbox, Box, Google
|
||||||
Drive and remote URLs
|
Drive and remote URLs
|
||||||
- [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins
|
- [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins
|
||||||
with React apps
|
with React apps
|
||||||
|
|
@ -153,7 +153,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||||
|
|
||||||
- [`Dashboard`](https://uppy.io/docs/dashboard/) — universal UI with previews,
|
- [`Dashboard`](https://uppy.io/docs/dashboard/) — universal UI with previews,
|
||||||
progress bars, metadata editor and all the cool stuff. Required for most UI
|
progress bars, metadata editor and all the cool stuff. Required for most UI
|
||||||
plugins like Webcam and Instagram
|
plugins like Webcam
|
||||||
- Headless components ([react](https://uppy.io/docs/react/), [svelte](https://uppy.io/docs/svelte/), [vue](https://uppy.io/docs/vue/))
|
- Headless components ([react](https://uppy.io/docs/react/), [svelte](https://uppy.io/docs/svelte/), [vue](https://uppy.io/docs/vue/))
|
||||||
|
|
||||||
### Sources
|
### Sources
|
||||||
|
|
@ -166,8 +166,6 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||||
Google Drive
|
Google Drive
|
||||||
- ⓒ [`Dropbox`](https://uppy.io/docs/dropbox/) — import files from Dropbox
|
- ⓒ [`Dropbox`](https://uppy.io/docs/dropbox/) — import files from Dropbox
|
||||||
- ⓒ [`Box`](https://uppy.io/docs/box/) — import files from Box
|
- ⓒ [`Box`](https://uppy.io/docs/box/) — import files from Box
|
||||||
- ⓒ [`Instagram`](https://uppy.io/docs/instagram/) — import images and videos
|
|
||||||
from Instagram
|
|
||||||
- ⓒ [`Facebook`](https://uppy.io/docs/facebook/) — import images and videos from
|
- ⓒ [`Facebook`](https://uppy.io/docs/facebook/) — import images and videos from
|
||||||
Facebook
|
Facebook
|
||||||
- ⓒ [`OneDrive`](https://uppy.io/docs/onedrive/) — import files from Microsoft
|
- ⓒ [`OneDrive`](https://uppy.io/docs/onedrive/) — import files from Microsoft
|
||||||
|
|
@ -222,7 +220,7 @@ cases, the file input leaves some to be desired:
|
||||||
- Uppy supports editing meta information before uploading.
|
- Uppy supports editing meta information before uploading.
|
||||||
- Uppy allows cropping images before uploading.
|
- Uppy allows cropping images before uploading.
|
||||||
- There’s the situation where people are using their mobile devices and want to
|
- There’s the situation where people are using their mobile devices and want to
|
||||||
upload on the go, but they have their picture on Instagram, files in Dropbox
|
upload on the go, but they have files in Dropbox
|
||||||
or a plain file URL from anywhere on the open web. Uppy allows to pick files
|
or a plain file URL from anywhere on the open web. Uppy allows to pick files
|
||||||
from those and push it to the destination without downloading it to your
|
from those and push it to the destination without downloading it to your
|
||||||
mobile device first.
|
mobile device first.
|
||||||
|
|
@ -272,7 +270,7 @@ If you want resumability with the Tus plugin, use
|
||||||
[one of the tus server implementations](https://tus.io/implementations.html) 👌🏼
|
[one of the tus server implementations](https://tus.io/implementations.html) 👌🏼
|
||||||
|
|
||||||
And you’ll need [`@uppy/companion`](https://uppy.io/docs/companion) if you’d
|
And you’ll need [`@uppy/companion`](https://uppy.io/docs/companion) if you’d
|
||||||
like your users to be able to pick files from Instagram, Google Drive, Dropbox
|
like your users to be able to pick files from Google Drive, Dropbox
|
||||||
or via direct URLs (with more services coming).
|
or via direct URLs (with more services coming).
|
||||||
|
|
||||||
## Contributions are welcome
|
## Contributions are welcome
|
||||||
|
|
|
||||||
68
biome.json
68
biome.json
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|
@ -14,10 +14,11 @@
|
||||||
"bin/**",
|
"bin/**",
|
||||||
"e2e/**",
|
"e2e/**",
|
||||||
"private/**",
|
"private/**",
|
||||||
"!packages/**/{dist,lib}/**",
|
"!packages/**/{dist,lib}",
|
||||||
"!node_modules",
|
"!node_modules",
|
||||||
"!.svelte-kit",
|
"!.svelte-kit",
|
||||||
"!packages/@uppy/components/src/input.css"
|
"!packages/@uppy/components/src/input.css",
|
||||||
|
"!examples/angular/**/*.html"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|
@ -25,20 +26,11 @@
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"formatWithErrors": true
|
"formatWithErrors": true
|
||||||
},
|
},
|
||||||
|
"html": {
|
||||||
|
"experimentalFullSupportEnabled": true
|
||||||
|
},
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"includes": [
|
|
||||||
"packages/**",
|
|
||||||
"examples/**",
|
|
||||||
"scripts/**",
|
|
||||||
"private/**",
|
|
||||||
"!packages/**/{dist,lib}/**",
|
|
||||||
"!node_modules",
|
|
||||||
"!.svelte-kit",
|
|
||||||
"!packages/@uppy/components/src/input.css",
|
|
||||||
"!**/*.vue",
|
|
||||||
"!**/*.svelte"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"recommended": true,
|
"recommended": true,
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
|
|
@ -48,22 +40,23 @@
|
||||||
"useExhaustiveDependencies": "error",
|
"useExhaustiveDependencies": "error",
|
||||||
"useHookAtTopLevel": "error",
|
"useHookAtTopLevel": "error",
|
||||||
"noUnusedFunctionParameters": "off",
|
"noUnusedFunctionParameters": "off",
|
||||||
|
"noUnusedImports": "error",
|
||||||
|
"noUnusedPrivateClassMembers": "error",
|
||||||
"noUnusedVariables": {
|
"noUnusedVariables": {
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"options": {
|
"options": {
|
||||||
"ignoreRestSiblings": true
|
"ignoreRestSiblings": true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"noNestedComponentDefinitions": "error",
|
||||||
|
"noReactPropAssignments": "error"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
"noNonNullAssertion": "off"
|
"noNonNullAssertion": "off"
|
||||||
},
|
},
|
||||||
"a11y": {
|
"a11y": {
|
||||||
"noSvgWithoutTitle": "off"
|
"noSvgWithoutTitle": "off",
|
||||||
},
|
"useSemanticElements": "off"
|
||||||
"nursery": {
|
|
||||||
"noNestedComponentDefinitions": "error",
|
|
||||||
"noReactPropAssign": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -80,5 +73,36 @@
|
||||||
"organizeImports": "on"
|
"organizeImports": "on"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"includes": ["packages/@uppy/companion/**"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"complexity": {
|
||||||
|
"useLiteralKeys": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["**/*.vue", "**/*.svelte", "packages/@uppy/vue/**/*.ts"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"correctness": {
|
||||||
|
"useHookAtTopLevel": "off",
|
||||||
|
"useExhaustiveDependencies": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"includes": ["examples/nextjs/**/*.css"],
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@uppy/companion": "workspace:*",
|
"@uppy/companion": "workspace:*",
|
||||||
"body-parser": "^1.20.4",
|
"body-parser": "^2.2.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"vite": "^7.1.11"
|
"vite": "^7.1.11"
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,4 @@ const server = app.listen(3020, () => {
|
||||||
console.log('listening on port 3020')
|
console.log('listening on port 3020')
|
||||||
})
|
})
|
||||||
|
|
||||||
companion.socket(server)
|
companion.socket(server, options)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
"@aws-sdk/s3-request-presigner": "^3.338.0",
|
"@aws-sdk/s3-request-presigner": "^3.338.0",
|
||||||
"body-parser": "^1.20.4",
|
"body-parser": "^1.20.4",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.22.0"
|
"express": "^5.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Uppy – AWS upload example</title>
|
<title>Uppy – AWS upload example</title>
|
||||||
|
|
@ -24,7 +24,8 @@
|
||||||
</footer>
|
</footer>
|
||||||
<noscript>You need JavaScript to run this example.</noscript>
|
<noscript>You need JavaScript to run this example.</noscript>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
import { AwsS3, Dashboard, Uppy } from './uppy.min.mjs'
|
||||||
|
|
||||||
function onUploadComplete(result) {
|
function onUploadComplete(result) {
|
||||||
console.log(
|
console.log(
|
||||||
'Upload complete! We’ve uploaded these files:',
|
'Upload complete! We’ve uploaded these files:',
|
||||||
|
|
@ -34,7 +35,7 @@
|
||||||
function onUploadSuccess(file, data) {
|
function onUploadSuccess(file, data) {
|
||||||
console.log(
|
console.log(
|
||||||
'Upload success! We’ve uploaded this file:',
|
'Upload success! We’ve uploaded this file:',
|
||||||
file.meta['name'],
|
file.meta.name,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Uppy – AWS upload example</title>
|
<title>Uppy – AWS upload example</title>
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
</footer>
|
</footer>
|
||||||
<noscript>You need JavaScript to run this example.</noscript>
|
<noscript>You need JavaScript to run this example.</noscript>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
import { AwsS3, Dashboard, Uppy } from './uppy.min.mjs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This generator transforms a deep object into URL-encodable pairs
|
* This generator transforms a deep object into URL-encodable pairs
|
||||||
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
||||||
|
|
@ -258,7 +259,7 @@
|
||||||
uppy.on('upload-success', (file, data) => {
|
uppy.on('upload-success', (file, data) => {
|
||||||
console.log(
|
console.log(
|
||||||
'Upload success! We’ve uploaded this file:',
|
'Upload success! We’ve uploaded this file:',
|
||||||
file.meta['name'],
|
file.meta.name,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need JavaScript enabled for this example to work.</noscript>
|
<noscript>You need JavaScript enabled for this example to work.</noscript>
|
||||||
<button id="uppyModalOpener">Open Modal</button>
|
<button type="button" id="uppyModalOpener">Open Modal</button>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {
|
import {
|
||||||
Uppy,
|
|
||||||
Dashboard,
|
Dashboard,
|
||||||
Webcam,
|
|
||||||
Tus,
|
Tus,
|
||||||
|
Uppy,
|
||||||
|
Webcam,
|
||||||
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
||||||
|
|
||||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@
|
||||||
"@uppy/google-drive": "workspace:*",
|
"@uppy/google-drive": "workspace:*",
|
||||||
"@uppy/provider-views": "workspace:*",
|
"@uppy/provider-views": "workspace:*",
|
||||||
"@uppy/tus": "workspace:*",
|
"@uppy/tus": "workspace:*",
|
||||||
"preact": "^10.26.10"
|
"preact": "^10.29.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@uppy/companion": "workspace:*",
|
"@uppy/companion": "workspace:*",
|
||||||
"body-parser": "^1.20.4",
|
"body-parser": "^2.2.2",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"express-session": "^1.15.6",
|
"express-session": "^1.15.6",
|
||||||
"npm-run-all": "^4.1.2",
|
"npm-run-all": "^4.1.2",
|
||||||
"vite": "^7.1.11"
|
"vite": "^7.1.11"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
"cors": "^2.8.5"
|
"cors": "^2.8.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"vite": "^7.1.11"
|
"vite": "^7.1.11"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,18 @@
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<button id="uppyModalOpener">Open Modal</button>
|
<button type="button" id="uppyModalOpener">Open Modal</button>
|
||||||
<noscript>This web page requires JavaScript to work properly.</noscript>
|
<noscript>This web page requires JavaScript to work properly.</noscript>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {
|
import {
|
||||||
Uppy,
|
|
||||||
Dashboard,
|
Dashboard,
|
||||||
Instagram,
|
|
||||||
GoogleDrive,
|
GoogleDrive,
|
||||||
Tus,
|
Tus,
|
||||||
|
Uppy,
|
||||||
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
||||||
|
|
||||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||||
.use(Dashboard, { trigger: '#uppyModalOpener' })
|
.use(Dashboard, { trigger: '#uppyModalOpener' })
|
||||||
.use(Instagram, {
|
|
||||||
target: Dashboard,
|
|
||||||
companionUrl: 'http://localhost:3020',
|
|
||||||
})
|
|
||||||
.use(GoogleDrive, {
|
.use(GoogleDrive, {
|
||||||
target: Dashboard,
|
target: Dashboard,
|
||||||
companionUrl: 'http://localhost:3020',
|
companionUrl: 'http://localhost:3020',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uppy/companion": "workspace:*",
|
"@uppy/companion": "workspace:*",
|
||||||
"body-parser": "^1.20.4",
|
"body-parser": "^1.20.4",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"express-session": "^1.15.6",
|
"express-session": "^1.15.6",
|
||||||
"light-server": "^2.4.0"
|
"light-server": "^2.4.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,6 @@ const companionOptions = {
|
||||||
key: 'your google key',
|
key: 'your google key',
|
||||||
secret: 'your google secret',
|
secret: 'your google secret',
|
||||||
},
|
},
|
||||||
instagram: {
|
|
||||||
key: 'your instagram key',
|
|
||||||
secret: 'your instagram secret',
|
|
||||||
},
|
|
||||||
dropbox: {
|
dropbox: {
|
||||||
key: 'your dropbox key',
|
key: 'your dropbox key',
|
||||||
secret: 'your dropbox secret',
|
secret: 'your dropbox secret',
|
||||||
|
|
@ -75,7 +71,7 @@ app.use((err, req, res) => {
|
||||||
res.status(500).json({ message: err.message, error: err })
|
res.status(500).json({ message: err.message, error: err })
|
||||||
})
|
})
|
||||||
|
|
||||||
companion.socket(app.listen(3020))
|
companion.socket(app.listen(3020), companionOptions)
|
||||||
|
|
||||||
console.log('Welcome to Companion!')
|
console.log('Welcome to Companion!')
|
||||||
console.log(`Listening on http://0.0.0.0:${3020}`)
|
console.log(`Listening on http://0.0.0.0:${3020}`)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"serve": "vite preview"
|
"serve": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@uppy/core": "workspace:*",
|
"@uppy/core": "workspace:*",
|
||||||
"@uppy/react": "workspace:*",
|
"@uppy/react": "workspace:*",
|
||||||
"@uppy/remote-sources": "workspace:*",
|
"@uppy/remote-sources": "workspace:*",
|
||||||
|
|
@ -23,13 +23,14 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^4.1.6",
|
||||||
|
"@vitest/browser-playwright": "^4.1.6",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"playwright": "1.57.0",
|
"playwright": "1.60.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^7.1.11",
|
"vite": "^8.0.14",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.1.6",
|
||||||
"vitest-browser-react": "^1.0.0"
|
"vitest-browser-react": "^2.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
|
/** biome-ignore-all lint/correctness/useUniqueElementIds: it's fine */
|
||||||
import Uppy, { type UppyFile } from '@uppy/core'
|
import Uppy, { type UppyFile } from '@uppy/core'
|
||||||
import UppyImageEditor from '@uppy/image-editor'
|
import UppyImageEditor from '@uppy/image-editor'
|
||||||
import {
|
import {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { userEvent } from '@vitest/browser/context'
|
|
||||||
import { setupWorker } from 'msw/browser'
|
import { setupWorker } from 'msw/browser'
|
||||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||||
|
import { userEvent } from 'vitest/browser'
|
||||||
import { render } from 'vitest-browser-react'
|
import { render } from 'vitest-browser-react'
|
||||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||||
import App from '../src/App'
|
import App from '../src/App'
|
||||||
|
|
@ -19,7 +19,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
test('renders all main sections and upload button is initially disabled', async () => {
|
test('renders all main sections and upload button is initially disabled', async () => {
|
||||||
const screen = render(<App />)
|
const screen = await render(<App />)
|
||||||
|
|
||||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||||
|
|
@ -33,7 +33,7 @@ describe('App', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can add and remove files and upload', async () => {
|
test('can add and remove files and upload', async () => {
|
||||||
const screen = render(<App />)
|
const screen = await render(<App />)
|
||||||
|
|
||||||
const fileInput = document.getElementById(
|
const fileInput = document.getElementById(
|
||||||
'uppy-dropzone-file-input-uppy',
|
'uppy-dropzone-file-input-uppy',
|
||||||
|
|
@ -59,7 +59,7 @@ describe('App', () => {
|
||||||
|
|
||||||
describe('ScreenCapture Component', () => {
|
describe('ScreenCapture Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(<App />)
|
const screen = await render(<App />)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Screen Capture' }).click()
|
await screen.getByRole('button', { name: 'Screen Capture' }).click()
|
||||||
|
|
||||||
|
|
@ -90,7 +90,7 @@ describe('ScreenCapture Component', () => {
|
||||||
|
|
||||||
describe('Webcam Component', () => {
|
describe('Webcam Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(<App />)
|
const screen = await render(<App />)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Webcam' }).click()
|
await screen.getByRole('button', { name: 'Webcam' }).click()
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ describe('Webcam Component', () => {
|
||||||
|
|
||||||
describe('RemoteSource Component', () => {
|
describe('RemoteSource Component', () => {
|
||||||
test('renders login button and login interaction works', async () => {
|
test('renders login button and login interaction works', async () => {
|
||||||
const screen = render(<App />)
|
const screen = await render(<App />)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Dropbox' }).click()
|
await screen.getByRole('button', { name: 'Dropbox' }).click()
|
||||||
|
|
||||||
|
|
@ -129,6 +129,5 @@ describe('RemoteSource Component', () => {
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
await expect.element(loginButton).toBeInTheDocument()
|
||||||
|
|
||||||
await loginButton.click()
|
await loginButton.click()
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,11 @@ import { defineConfig } from 'vite'
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
// @vitejs/plugin-react v5+ no longer adds react/react-dom to resolve.dedupe
|
||||||
|
// automatically (it did in v4). Without deduping, the linked @uppy/react
|
||||||
|
// workspace pulls in a second React copy, causing "Invalid hook call".
|
||||||
|
// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md (v5.0.0)
|
||||||
|
resolve: {
|
||||||
|
dedupe: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,21 @@
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { playwright } from '@vitest/browser-playwright'
|
||||||
import { defineConfig } from 'vitest/config'
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
// @vitejs/plugin-react v5+ no longer adds react/react-dom to resolve.dedupe
|
||||||
|
// automatically (it did in v4). Without deduping, the linked @uppy/react
|
||||||
|
// workspace pulls in a second React copy, causing "Invalid hook call".
|
||||||
|
// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md (v5.0.0)
|
||||||
|
resolve: {
|
||||||
|
dedupe: ['react', 'react-dom'],
|
||||||
|
},
|
||||||
test: {
|
test: {
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
provider: 'playwright',
|
provider: playwright(),
|
||||||
instances: [{ browser: 'chromium' }],
|
instances: [{ browser: 'chromium' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,14 @@
|
||||||
"@uppy/transloadit": "workspace:*",
|
"@uppy/transloadit": "workspace:*",
|
||||||
"@uppy/tus": "workspace:*",
|
"@uppy/tus": "workspace:*",
|
||||||
"@uppy/xhr-upload": "workspace:*",
|
"@uppy/xhr-upload": "workspace:*",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^7.12.0"
|
"react-router": "^7.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-router/dev": "^7.12.0",
|
"@react-router/dev": "^7.12.0",
|
||||||
"@types/express": "^4.17.20",
|
"@types/express": "^5.0.6",
|
||||||
"@types/react": "^18.2.20",
|
"@types/react": "^18.2.20",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"tsx": "^4.0.0",
|
"tsx": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ async function startServer() {
|
||||||
|
|
||||||
// TUS upload endpoints (before React Router)
|
// TUS upload endpoints (before React Router)
|
||||||
app.all('/api/upload/tus', (req, res) => tusServer.handle(req, res))
|
app.all('/api/upload/tus', (req, res) => tusServer.handle(req, res))
|
||||||
app.all('/api/upload/tus/*', (req, res) => tusServer.handle(req, res))
|
app.all('/api/upload/tus/{*splat}', (req, res) =>
|
||||||
|
tusServer.handle(req, res),
|
||||||
|
)
|
||||||
|
|
||||||
// Handle Chrome DevTools requests silently
|
// Handle Chrome DevTools requests silently
|
||||||
app.get('/.well-known/appspecific/com.chrome.devtools.json', (req, res) => {
|
app.get('/.well-known/appspecific/com.chrome.devtools.json', (req, res) => {
|
||||||
|
|
@ -49,7 +51,7 @@ async function startServer() {
|
||||||
})
|
})
|
||||||
|
|
||||||
// React Router handles all other routes
|
// React Router handles all other routes
|
||||||
app.all('*', reactRouterHandler)
|
app.all('/{*splat}', reactRouterHandler)
|
||||||
|
|
||||||
const port = process.env.PORT || 3000
|
const port = process.env.PORT || 3000
|
||||||
const server = app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,16 @@
|
||||||
"@sveltejs/kit": "^2.49.5",
|
"@sveltejs/kit": "^2.49.5",
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^4.1.6",
|
||||||
|
"@vitest/browser-playwright": "^4.1.6",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"playwright": "1.57.0",
|
"playwright": "1.60.0",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^6.2.5",
|
"vite": "^6.2.5",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.1.6",
|
||||||
"vitest-browser-svelte": "^1.0.0"
|
"vitest-browser-svelte": "^2.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center gap-4">
|
<div class="flex items-center justify-center gap-4">
|
||||||
<input {...getFileInputProps()} class="hidden" />
|
<input {...getFileInputProps()} class="hidden" />
|
||||||
<button
|
<button type="button"
|
||||||
{...getButtonProps()}
|
{...getButtonProps()}
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -30,7 +30,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||||
Device
|
Device
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
onclick={() => openModal('webcam')}
|
onclick={() => openModal('webcam')}
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -40,7 +40,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||||
Webcam
|
Webcam
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
onclick={() => openModal('screen-capture')}
|
onclick={() => openModal('screen-capture')}
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -50,7 +50,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||||
Screen Capture
|
Screen Capture
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
onclick={() => openModal('dropbox')}
|
onclick={() => openModal('dropbox')}
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const editor = useImageEditor({ file })
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
<!-- biome-ignore lint/a11y/useAltText: alt is provided via editor.getImageProps() spread -->
|
||||||
<img
|
<img
|
||||||
class="w-full max-h-[400px] rounded-lg border-2"
|
class="w-full max-h-[400px] rounded-lg border-2"
|
||||||
{...editor.getImageProps()}
|
{...editor.getImageProps()}
|
||||||
|
|
@ -41,31 +42,31 @@ const editor = useImageEditor({ file })
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap mb-4">
|
<div class="flex gap-2 flex-wrap mb-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getRotateButtonProps(-90)}
|
{...editor.getRotateButtonProps(-90)}
|
||||||
>
|
>
|
||||||
↶ -90°
|
↶ -90°
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getRotateButtonProps(90)}
|
{...editor.getRotateButtonProps(90)}
|
||||||
>
|
>
|
||||||
↷ +90°
|
↷ +90°
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getFlipHorizontalButtonProps()}
|
{...editor.getFlipHorizontalButtonProps()}
|
||||||
>
|
>
|
||||||
⇆ Flip
|
⇆ Flip
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getZoomButtonProps(0.1)}
|
{...editor.getZoomButtonProps(0.1)}
|
||||||
>
|
>
|
||||||
+ Zoom
|
+ Zoom
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getZoomButtonProps(-0.1)}
|
{...editor.getZoomButtonProps(-0.1)}
|
||||||
>
|
>
|
||||||
|
|
@ -74,25 +75,25 @@ const editor = useImageEditor({ file })
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap mb-4">
|
<div class="flex gap-2 flex-wrap mb-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getCropSquareButtonProps()}
|
{...editor.getCropSquareButtonProps()}
|
||||||
>
|
>
|
||||||
1:1
|
1:1
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getCropLandscapeButtonProps()}
|
{...editor.getCropLandscapeButtonProps()}
|
||||||
>
|
>
|
||||||
16:9
|
16:9
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getCropPortraitButtonProps()}
|
{...editor.getCropPortraitButtonProps()}
|
||||||
>
|
>
|
||||||
9:16
|
9:16
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
{...editor.getResetButtonProps()}
|
{...editor.getResetButtonProps()}
|
||||||
>
|
>
|
||||||
|
|
@ -101,13 +102,13 @@ const editor = useImageEditor({ file })
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 justify-end">
|
<div class="flex gap-4 justify-end">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
||||||
{...editor.getCancelButtonProps({ onClick: close })}
|
{...editor.getCancelButtonProps({ onClick: close })}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||||
{...editor.getSaveButtonProps({ onClick: close })}
|
{...editor.getSaveButtonProps({ onClick: close })}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -42,36 +42,37 @@ const {
|
||||||
<p>{mediaError.message ? `Camera error: ${mediaError.message}` : 'An unknown camera error occurred.'}</p>
|
<p>{mediaError.message ? `Camera error: ${mediaError.message}` : 'An unknown camera error occurred.'}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- biome-ignore lint/a11y/useMediaCaption: live camera preview does not require captions -->
|
||||||
<video
|
<video
|
||||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||||
{...videoProps}
|
{...videoProps}
|
||||||
></video>
|
></video>
|
||||||
<div class="flex gap-4 flex-wrap mt-4">
|
<div class="flex gap-4 flex-wrap mt-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||||
{...primaryActionButtonProps}
|
{...primaryActionButtonProps}
|
||||||
>
|
>
|
||||||
{primaryActionButtonLabel}
|
{primaryActionButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||||
{...recordButtonProps}
|
{...recordButtonProps}
|
||||||
>
|
>
|
||||||
Record
|
Record
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||||
{...stopRecordingButtonProps}
|
{...stopRecordingButtonProps}
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||||
{...submitButtonProps}
|
{...submitButtonProps}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||||
{...discardButtonProps}
|
{...discardButtonProps}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const dtf = new Intl.DateTimeFormat('en-US', {
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used as a Svelte action via use:setFolderCheckboxIndeterminate
|
||||||
function setFolderCheckboxIndeterminate(
|
function setFolderCheckboxIndeterminate(
|
||||||
node: HTMLInputElement,
|
node: HTMLInputElement,
|
||||||
item: PartialTreeFolderNode,
|
item: PartialTreeFolderNode,
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ let hideUploadButton = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="p-5 max-w-xl mx-auto">
|
<main class="p-5 max-w-xl mx-auto">
|
||||||
<button onclick={() => (disabled = !disabled)}>Toggle dashboard</button>
|
<button type="button" onclick={() => (disabled = !disabled)}>Toggle dashboard</button>
|
||||||
<Dashboard props={{disabled: disabled}} uppy={uppy} />
|
<Dashboard props={{disabled: disabled}} uppy={uppy} />
|
||||||
<button onclick={() => (hideUploadButton = !hideUploadButton)}>Toggle statusbar</button>
|
<button type="button" onclick={() => (hideUploadButton = !hideUploadButton)}>Toggle statusbar</button>
|
||||||
<div id="statusbar-container">
|
<div id="statusbar-container">
|
||||||
<StatusBar props={{ hideUploadButton }} uppy={uppy} />
|
<StatusBar props={{ hideUploadButton }} uppy={uppy} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { userEvent } from '@vitest/browser/context'
|
|
||||||
import { setupWorker } from 'msw/browser'
|
import { setupWorker } from 'msw/browser'
|
||||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||||
|
import { userEvent } from 'vitest/browser'
|
||||||
import { render } from 'vitest-browser-svelte'
|
import { render } from 'vitest-browser-svelte'
|
||||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||||
import PropsReactivity from '../src/components/test/props-reactivity.svelte'
|
import PropsReactivity from '../src/components/test/props-reactivity.svelte'
|
||||||
|
|
@ -20,7 +20,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
test('renders all main sections and upload button is initially disabled', async () => {
|
test('renders all main sections and upload button is initially disabled', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||||
|
|
@ -34,7 +34,7 @@ describe('App', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can add and remove files and upload', async () => {
|
test('can add and remove files and upload', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
const fileInput = document.getElementById(
|
const fileInput = document.getElementById(
|
||||||
'uppy-dropzone-file-input-uppy',
|
'uppy-dropzone-file-input-uppy',
|
||||||
|
|
@ -60,7 +60,7 @@ describe('App', () => {
|
||||||
|
|
||||||
describe('ScreenCapture Component', () => {
|
describe('ScreenCapture Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen
|
await screen
|
||||||
.getByRole('button', { name: 'Screen Capture', exact: true })
|
.getByRole('button', { name: 'Screen Capture', exact: true })
|
||||||
|
|
@ -93,7 +93,7 @@ describe('ScreenCapture Component', () => {
|
||||||
|
|
||||||
describe('Webcam Component', () => {
|
describe('Webcam Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
||||||
|
|
||||||
|
|
@ -124,7 +124,7 @@ describe('Webcam Component', () => {
|
||||||
|
|
||||||
describe('RemoteSource Component', () => {
|
describe('RemoteSource Component', () => {
|
||||||
test('renders login button and login interaction works', async () => {
|
test('renders login button and login interaction works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
||||||
|
|
||||||
|
|
@ -132,12 +132,11 @@ describe('RemoteSource Component', () => {
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
await expect.element(loginButton).toBeInTheDocument()
|
||||||
|
|
||||||
await loginButton.click()
|
await loginButton.click()
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Dashboard reacts to prop changes', async () => {
|
test('Dashboard reacts to prop changes', async () => {
|
||||||
const screen = render(PropsReactivity)
|
const screen = await render(PropsReactivity)
|
||||||
const toggleButton = screen.getByText('Toggle dashboard')
|
const toggleButton = screen.getByText('Toggle dashboard')
|
||||||
const dashboard = screen.container.querySelector('.uppy-Dashboard')
|
const dashboard = screen.container.querySelector('.uppy-Dashboard')
|
||||||
|
|
||||||
|
|
@ -148,7 +147,7 @@ test('Dashboard reacts to prop changes', async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('StatusBar reacts to prop changes', async () => {
|
test('StatusBar reacts to prop changes', async () => {
|
||||||
const screen = render(PropsReactivity)
|
const screen = await render(PropsReactivity)
|
||||||
const toggleButton = screen.getByText('Toggle statusbar')
|
const toggleButton = screen.getByText('Toggle statusbar')
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { sveltekit } from '@sveltejs/kit/vite'
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { playwright } from '@vitest/browser-playwright'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
@ -7,7 +8,7 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
provider: 'playwright',
|
provider: playwright(),
|
||||||
instances: [{ browser: 'chromium' }],
|
instances: [{ browser: 'chromium' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
This API is a one-shot upload UI using a modal overlay. Call the
|
This API is a one-shot upload UI using a modal overlay. Call the
|
||||||
function and receive a listen to an event with upload results ✌️
|
function and receive a listen to an event with upload results ✌️
|
||||||
</p>
|
</p>
|
||||||
<button onclick="openModal()">Open</button>
|
<button type="button" onclick="openModal()">Open</button>
|
||||||
|
|
||||||
<h2>uppy.upload()</h2>
|
<h2>uppy.upload()</h2>
|
||||||
<p>An <input type=file> backed by <code>uppy.upload()</code>:</p>
|
<p>An <input type=file> backed by <code>uppy.upload()</code>:</p>
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,11 @@ const formUppy = new Uppy({
|
||||||
})
|
})
|
||||||
.use(Transloadit, {
|
.use(Transloadit, {
|
||||||
waitForEncoding: true,
|
waitForEncoding: true,
|
||||||
params: {
|
assemblyOptions: {
|
||||||
auth: { key: TRANSLOADIT_KEY },
|
params: {
|
||||||
template_id: TEMPLATE_ID,
|
auth: { key: TRANSLOADIT_KEY },
|
||||||
|
template_id: TEMPLATE_ID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -100,9 +102,11 @@ const formUppyWithDashboard = new Uppy({
|
||||||
})
|
})
|
||||||
.use(Transloadit, {
|
.use(Transloadit, {
|
||||||
waitForEncoding: true,
|
waitForEncoding: true,
|
||||||
params: {
|
assemblyOptions: {
|
||||||
auth: { key: TRANSLOADIT_KEY },
|
params: {
|
||||||
template_id: TEMPLATE_ID,
|
auth: { key: TRANSLOADIT_KEY },
|
||||||
|
template_id: TEMPLATE_ID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -129,9 +133,11 @@ const dashboard = new Uppy({
|
||||||
.use(ImageEditor, { target: Dashboard })
|
.use(ImageEditor, { target: Dashboard })
|
||||||
.use(Transloadit, {
|
.use(Transloadit, {
|
||||||
waitForEncoding: true,
|
waitForEncoding: true,
|
||||||
params: {
|
assemblyOptions: {
|
||||||
auth: { key: TRANSLOADIT_KEY },
|
params: {
|
||||||
template_id: TEMPLATE_ID,
|
auth: { key: TRANSLOADIT_KEY },
|
||||||
|
template_id: TEMPLATE_ID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -151,9 +157,11 @@ const dashboardModal = new Uppy({
|
||||||
.use(ImageEditor, { target: Dashboard })
|
.use(ImageEditor, { target: Dashboard })
|
||||||
.use(Transloadit, {
|
.use(Transloadit, {
|
||||||
waitForEncoding: true,
|
waitForEncoding: true,
|
||||||
params: {
|
assemblyOptions: {
|
||||||
auth: { key: TRANSLOADIT_KEY },
|
params: {
|
||||||
template_id: TEMPLATE_ID,
|
auth: { key: TRANSLOADIT_KEY },
|
||||||
|
template_id: TEMPLATE_ID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -182,9 +190,11 @@ const uppyWithoutUI = new Uppy({
|
||||||
},
|
},
|
||||||
}).use(Transloadit, {
|
}).use(Transloadit, {
|
||||||
waitForEncoding: true,
|
waitForEncoding: true,
|
||||||
params: {
|
assemblyOptions: {
|
||||||
auth: { key: TRANSLOADIT_KEY },
|
params: {
|
||||||
template_id: TEMPLATE_ID,
|
auth: { key: TRANSLOADIT_KEY },
|
||||||
|
template_id: TEMPLATE_ID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"@uppy/remote-sources": "workspace:*",
|
"@uppy/remote-sources": "workspace:*",
|
||||||
"@uppy/transloadit": "workspace:*",
|
"@uppy/transloadit": "workspace:*",
|
||||||
"@uppy/webcam": "workspace:*",
|
"@uppy/webcam": "workspace:*",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"he": "^1.2.0"
|
"he": "^1.2.0"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ function Footer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormFields(fields) {
|
function FormFields(fields) {
|
||||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||||
function Field([name, value]) {
|
function Field([name, value]) {
|
||||||
if (name === 'transloadit') return ''
|
if (name === 'transloadit') return ''
|
||||||
let isValueJSON = false
|
let isValueJSON = false
|
||||||
|
|
@ -76,7 +76,7 @@ function FormFields(fields) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UploadsList(uploads) {
|
function UploadsList(uploads) {
|
||||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||||
function Upload(upload) {
|
function Upload(upload) {
|
||||||
return `<li>${e(upload.name)}</li>`
|
return `<li>${e(upload.name)}</li>`
|
||||||
}
|
}
|
||||||
|
|
@ -89,12 +89,12 @@ function UploadsList(uploads) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResultsList(results) {
|
function ResultsList(results) {
|
||||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||||
function Result(result) {
|
function Result(result) {
|
||||||
return `<li>${e(result.name)} <a href="${result.ssl_url}" target="_blank">View</a></li>`
|
return `<li>${e(result.name)} <a href="${result.ssl_url}" target="_blank">View</a></li>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||||
function ResultsSection(stepName) {
|
function ResultsSection(stepName) {
|
||||||
return `
|
return `
|
||||||
<h2>${e(stepName)}</h2>
|
<h2>${e(stepName)}</h2>
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,13 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^4.1.6",
|
||||||
|
"@vitest/browser-playwright": "^4.1.6",
|
||||||
"msw": "^2.10.4",
|
"msw": "^2.10.4",
|
||||||
"playwright": "1.57.0",
|
"playwright": "1.60.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.1.11",
|
"vite": "^7.1.11",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.1.6",
|
||||||
"vitest-browser-vue": "^1.0.0"
|
"vitest-browser-vue": "^2.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-center gap-4">
|
<div class="flex items-center justify-center gap-4">
|
||||||
<input v-bind="getFileInputProps()" class="hidden" />
|
<input v-bind="getFileInputProps()" class="hidden" />
|
||||||
<button
|
<button type="button"
|
||||||
v-bind="getButtonProps()"
|
v-bind="getButtonProps()"
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
Device
|
Device
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
@click="() => props.openModal('webcam')"
|
@click="() => props.openModal('webcam')"
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
Webcam
|
Webcam
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
@click="() => props.openModal('screen-capture')"
|
@click="() => props.openModal('screen-capture')"
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
Screen Capture
|
Screen Capture
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button type="button"
|
||||||
@click="() => props.openModal('dropbox')"
|
@click="() => props.openModal('dropbox')"
|
||||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
<!-- biome-ignore lint/a11y/useAltText: alt is provided via editor.getImageProps() spread -->
|
||||||
<img
|
<img
|
||||||
class="w-full max-h-[400px] rounded-lg border-2"
|
class="w-full max-h-[400px] rounded-lg border-2"
|
||||||
v-bind="editor.getImageProps()"
|
v-bind="editor.getImageProps()"
|
||||||
|
|
@ -29,31 +30,31 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap mb-4">
|
<div class="flex gap-2 flex-wrap mb-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getRotateButtonProps(-90)"
|
v-bind="editor.getRotateButtonProps(-90)"
|
||||||
>
|
>
|
||||||
↶ -90°
|
↶ -90°
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getRotateButtonProps(90)"
|
v-bind="editor.getRotateButtonProps(90)"
|
||||||
>
|
>
|
||||||
↷ +90°
|
↷ +90°
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getFlipHorizontalButtonProps()"
|
v-bind="editor.getFlipHorizontalButtonProps()"
|
||||||
>
|
>
|
||||||
⇆ Flip
|
⇆ Flip
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getZoomButtonProps(0.1)"
|
v-bind="editor.getZoomButtonProps(0.1)"
|
||||||
>
|
>
|
||||||
+ Zoom
|
+ Zoom
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getZoomButtonProps(-0.1)"
|
v-bind="editor.getZoomButtonProps(-0.1)"
|
||||||
>
|
>
|
||||||
|
|
@ -62,25 +63,25 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap mb-4">
|
<div class="flex gap-2 flex-wrap mb-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getCropSquareButtonProps()"
|
v-bind="editor.getCropSquareButtonProps()"
|
||||||
>
|
>
|
||||||
1:1
|
1:1
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getCropLandscapeButtonProps()"
|
v-bind="editor.getCropLandscapeButtonProps()"
|
||||||
>
|
>
|
||||||
16:9
|
16:9
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getCropPortraitButtonProps()"
|
v-bind="editor.getCropPortraitButtonProps()"
|
||||||
>
|
>
|
||||||
9:16
|
9:16
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||||
v-bind="editor.getResetButtonProps()"
|
v-bind="editor.getResetButtonProps()"
|
||||||
>
|
>
|
||||||
|
|
@ -89,13 +90,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 justify-end">
|
<div class="flex gap-4 justify-end">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
||||||
v-bind="editor.getCancelButtonProps({ onClick: close })"
|
v-bind="editor.getCancelButtonProps({ onClick: close })"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||||
v-bind="editor.getSaveButtonProps({ onClick: close })"
|
v-bind="editor.getSaveButtonProps({ onClick: close })"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -24,31 +24,31 @@
|
||||||
<track kind="captions" />
|
<track kind="captions" />
|
||||||
</video>
|
</video>
|
||||||
<div class="flex gap-4 flex-wrap mt-4">
|
<div class="flex gap-4 flex-wrap mt-4">
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||||
v-bind="primaryActionButtonProps"
|
v-bind="primaryActionButtonProps"
|
||||||
>
|
>
|
||||||
{{ primaryActionButtonLabel }}
|
{{ primaryActionButtonLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||||
v-bind="recordButtonProps"
|
v-bind="recordButtonProps"
|
||||||
>
|
>
|
||||||
Record
|
Record
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||||
v-bind="stopRecordingButtonProps"
|
v-bind="stopRecordingButtonProps"
|
||||||
>
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||||
v-bind="submitButtonProps"
|
v-bind="submitButtonProps"
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button"
|
||||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||||
v-bind="discardButtonProps"
|
v-bind="discardButtonProps"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { userEvent } from '@vitest/browser/context'
|
|
||||||
import { setupWorker } from 'msw/browser'
|
import { setupWorker } from 'msw/browser'
|
||||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||||
|
import { userEvent } from 'vitest/browser'
|
||||||
import { render } from 'vitest-browser-vue'
|
import { render } from 'vitest-browser-vue'
|
||||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||||
import App from '../src/App.vue'
|
import App from '../src/App.vue'
|
||||||
|
|
@ -19,7 +19,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
||||||
|
|
||||||
describe('App', () => {
|
describe('App', () => {
|
||||||
test('renders all main sections and upload button is initially disabled', async () => {
|
test('renders all main sections and upload button is initially disabled', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||||
|
|
@ -33,7 +33,7 @@ describe('App', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can add and remove files and upload', async () => {
|
test('can add and remove files and upload', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
const fileInput = document.getElementById(
|
const fileInput = document.getElementById(
|
||||||
'uppy-dropzone-file-input-uppy',
|
'uppy-dropzone-file-input-uppy',
|
||||||
|
|
@ -59,7 +59,7 @@ describe('App', () => {
|
||||||
|
|
||||||
describe('ScreenCapture Component', () => {
|
describe('ScreenCapture Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen
|
await screen
|
||||||
.getByRole('button', { name: 'Screen Capture', exact: true })
|
.getByRole('button', { name: 'Screen Capture', exact: true })
|
||||||
|
|
@ -92,7 +92,7 @@ describe('ScreenCapture Component', () => {
|
||||||
|
|
||||||
describe('Webcam Component', () => {
|
describe('Webcam Component', () => {
|
||||||
test('renders with title, control buttons, and close functionality works', async () => {
|
test('renders with title, control buttons, and close functionality works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
||||||
|
|
||||||
|
|
@ -123,7 +123,7 @@ describe('Webcam Component', () => {
|
||||||
|
|
||||||
describe('RemoteSource Component', () => {
|
describe('RemoteSource Component', () => {
|
||||||
test('renders login button and login interaction works', async () => {
|
test('renders login button and login interaction works', async () => {
|
||||||
const screen = render(App)
|
const screen = await render(App)
|
||||||
|
|
||||||
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
||||||
|
|
||||||
|
|
@ -131,6 +131,5 @@ describe('RemoteSource Component', () => {
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
await expect.element(loginButton).toBeInTheDocument()
|
||||||
|
|
||||||
await loginButton.click()
|
await loginButton.click()
|
||||||
await expect.element(loginButton).toBeInTheDocument()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { playwright } from '@vitest/browser-playwright'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
@ -7,7 +8,7 @@ export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
provider: 'playwright',
|
provider: playwright(),
|
||||||
instances: [{ browser: 'chromium' }],
|
instances: [{ browser: 'chromium' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"@uppy/dashboard": "workspace:*",
|
"@uppy/dashboard": "workspace:*",
|
||||||
"@uppy/xhr-upload": "workspace:*",
|
"@uppy/xhr-upload": "workspace:*",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.22.0",
|
"express": "^5.2.1",
|
||||||
"multer": "^2.0.2"
|
"multer": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "@uppy-dev/build",
|
"name": "@uppy-dev/build",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
|
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox, Google Drive, S3 and more :dog:",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"examples/*",
|
"examples/*",
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
"release": "changeset publish"
|
"release": "changeset publish"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.5",
|
"@biomejs/biome": "2.4.15",
|
||||||
"@changesets/changelog-github": "^0.5.1",
|
"@changesets/changelog-github": "^0.5.1",
|
||||||
"@changesets/cli": "patch:@changesets/cli@npm%3A2.29.5#~/.yarn/patches/@changesets-cli-npm-2.29.5-68d8030bf3.patch",
|
"@changesets/cli": "patch:@changesets/cli@npm%3A2.29.5#~/.yarn/patches/@changesets-cli-npm-2.29.5-68d8030bf3.patch",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
{
|
|
||||||
"ignorePatterns": ["projects/**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts"],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@angular-eslint/recommended",
|
|
||||||
"plugin:@angular-eslint/template/process-inline-templates"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
// eslint-disable-line import/newline-after-import
|
|
||||||
"@angular-eslint/directive-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "attribute",
|
|
||||||
"prefix": "app",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
|
||||||
"import/no-unresolved": "off",
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"@angular-eslint/component-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "element",
|
|
||||||
"prefix": "app",
|
|
||||||
"style": "kebab-case"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"semi": ["error", "never"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"extends": [
|
|
||||||
"plugin:@angular-eslint/template/recommended",
|
|
||||||
"plugin:@angular-eslint/template/accessibility"
|
|
||||||
],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -17,22 +17,15 @@
|
||||||
"@angular/platform-browser": "^19.2.17",
|
"@angular/platform-browser": "^19.2.17",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.17",
|
"@angular/platform-browser-dynamic": "^19.2.17",
|
||||||
"@angular/router": "^19.2.17",
|
"@angular/router": "^19.2.17",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.8.1",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.0.0",
|
"@angular-devkit/build-angular": "^19.0.0",
|
||||||
"@angular-eslint/eslint-plugin": "^18.0.1",
|
|
||||||
"@angular-eslint/eslint-plugin-template": "^18.0.1",
|
|
||||||
"@angular/cli": "^19.0.0",
|
"@angular/cli": "^19.0.0",
|
||||||
"@angular/compiler-cli": "^19.2.17",
|
"@angular/compiler-cli": "^19.2.17",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
|
||||||
"@typescript-eslint/types": "^7.2.0",
|
|
||||||
"@typescript-eslint/utils": "^7.2.0",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../../.eslintrc.json",
|
|
||||||
"ignorePatterns": ["!**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts"],
|
|
||||||
"parserOptions": {
|
|
||||||
"project": [
|
|
||||||
"packages/@uppy/angular/projects/angular/tsconfig.lib.json",
|
|
||||||
"packages/@uppy/angular/projects/angular/tsconfig.spec.json"
|
|
||||||
],
|
|
||||||
"createDefaultProgram": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"@angular-eslint/component-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "element",
|
|
||||||
"prefix": "uppy",
|
|
||||||
"style": "kebab-case"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@angular-eslint/directive-selector": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"type": "attribute",
|
|
||||||
"prefix": "uppy",
|
|
||||||
"style": "camelCase"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dot-notation": "error",
|
|
||||||
"indent": "error",
|
|
||||||
"no-empty-function": "off",
|
|
||||||
"no-shadow": "error",
|
|
||||||
"no-unused-expressions": "error",
|
|
||||||
"no-use-before-define": "off",
|
|
||||||
"quotes": "error",
|
|
||||||
"semi": "error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["*.html"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
"prepublishOnly": "rm -fr * && cp -r ../../../dist/uppy/angular .."
|
"prepublishOnly": "rm -fr * && cp -r ../../../dist/uppy/angular .."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||||
|
|
|
||||||
|
|
@ -41,16 +41,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uppy/utils": "workspace:^",
|
"@uppy/utils": "workspace:^",
|
||||||
"preact": "^10.26.10"
|
"preact": "^10.29.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cssnano": "^7.0.7",
|
"cssnano": "^8.0.1",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^29.1.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.15",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.89.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^4.1.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@uppy/core": "workspace:^"
|
"@uppy/core": "workspace:^"
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,9 @@ export default class Audio<M extends Meta, B extends Body> extends UIPlugin<
|
||||||
#stop = async () => {
|
#stop = async () => {
|
||||||
if (this.#stream) {
|
if (this.#stream) {
|
||||||
const audioTracks = this.#stream.getAudioTracks()
|
const audioTracks = this.#stream.getAudioTracks()
|
||||||
audioTracks.forEach((track) => track.stop())
|
audioTracks.forEach((track) => {
|
||||||
|
track.stop()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#recorder) {
|
if (this.#recorder) {
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,12 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.362.0",
|
"@aws-sdk/client-s3": "^3.362.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.362.0",
|
"@aws-sdk/s3-request-presigner": "^3.362.0",
|
||||||
|
"@types/node": "^20.19.0",
|
||||||
"@uppy/core": "workspace:^",
|
"@uppy/core": "workspace:^",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"nock": "^13.1.0",
|
"nock": "^13.1.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^4.1.6",
|
||||||
"whatwg-fetch": "3.6.2"
|
"whatwg-fetch": "3.6.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -263,9 +263,9 @@ export class HTTPCommunicationQueue<M extends Meta, B extends Body> {
|
||||||
const data = chunk.getData()
|
const data = chunk.getData()
|
||||||
if (method.toUpperCase() === 'POST') {
|
if (method.toUpperCase() === 'POST') {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
Object.entries(fields!).forEach(([key, value]) =>
|
Object.entries(fields!).forEach(([key, value]) => {
|
||||||
formData.set(key, value),
|
formData.set(key, value)
|
||||||
)
|
})
|
||||||
formData.set('file', data)
|
formData.set('file', data)
|
||||||
body = formData
|
body = formData
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -256,6 +256,7 @@ class MultipartUploader<M extends Meta, B extends Body> {
|
||||||
else this.pause()
|
else this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: accessed via Symbol in tests
|
||||||
private [Symbol.for('uppy test: getChunkState')]() {
|
private [Symbol.for('uppy test: getChunkState')]() {
|
||||||
return this.#chunkState
|
return this.#chunkState
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -662,10 +662,11 @@ export default class AwsS3Multipart<
|
||||||
querify: true,
|
querify: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const query = new URLSearchParams({ filename, type, ...metadata } as Record<
|
const query = new URLSearchParams({
|
||||||
string,
|
filename,
|
||||||
string
|
...(type != null && { type }),
|
||||||
>)
|
...metadata,
|
||||||
|
})
|
||||||
|
|
||||||
return this.#client.get(`s3/params?${query}`, options)
|
return this.#client.get(`s3/params?${query}`, options)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "../../../tsconfig.shared",
|
"extends": "../../../tsconfig.shared",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
"noEmit": true
|
"noEmit": true,
|
||||||
|
"types": ["preact", "node"]
|
||||||
},
|
},
|
||||||
"include": ["./package.json", "./src/**/*.*"],
|
"include": ["./package.json", "./src/**/*.*"],
|
||||||
"references": [
|
"references": [
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"@uppy/companion-client": "workspace:^",
|
"@uppy/companion-client": "workspace:^",
|
||||||
"@uppy/provider-views": "workspace:^",
|
"@uppy/provider-views": "workspace:^",
|
||||||
"@uppy/utils": "workspace:^",
|
"@uppy/utils": "workspace:^",
|
||||||
"preact": "^10.26.10"
|
"preact": "^10.29.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@uppy/core": "workspace:^"
|
"@uppy/core": "workspace:^"
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,9 @@
|
||||||
"p-retry": "^6.1.0"
|
"p-retry": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^29.1.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^4.1.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@uppy/core": "workspace:^"
|
"@uppy/core": "workspace:^"
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,11 @@ import type {
|
||||||
UnknownProviderPlugin,
|
UnknownProviderPlugin,
|
||||||
Uppy,
|
Uppy,
|
||||||
} from '@uppy/core'
|
} from '@uppy/core'
|
||||||
import type { CompanionClientProvider, RequestOptions } from '@uppy/utils'
|
import {
|
||||||
import { isOriginAllowed } from './getAllowedHosts.js'
|
type CompanionClientProvider,
|
||||||
|
getSocketHost,
|
||||||
|
type RequestOptions,
|
||||||
|
} from '@uppy/utils'
|
||||||
import type { CompanionPluginOptions } from './index.js'
|
import type { CompanionPluginOptions } from './index.js'
|
||||||
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
|
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
|
||||||
|
|
||||||
|
|
@ -135,22 +138,27 @@ export default class Provider<M extends Meta, B extends Body>
|
||||||
authUrl({
|
authUrl({
|
||||||
authFormData,
|
authFormData,
|
||||||
query,
|
query,
|
||||||
|
authCallbackToken,
|
||||||
}: {
|
}: {
|
||||||
authFormData: unknown
|
authFormData: unknown
|
||||||
query: Record<string, string>
|
query: Record<string, string>
|
||||||
|
authCallbackToken?: string | undefined
|
||||||
}): string {
|
}): string {
|
||||||
const params = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
...query,
|
...query,
|
||||||
// This is only used for Companion instances configured to accept multiple origins.
|
// `origin` is only used for Companion instances configured to accept multiple origins.
|
||||||
state: btoa(JSON.stringify({ origin: getOrigin() })),
|
state: btoa(JSON.stringify({ origin: getOrigin() })),
|
||||||
...this.authQuery({ authFormData }),
|
...this.authQuery({ authFormData }),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.preAuthToken) {
|
if (this.preAuthToken) {
|
||||||
params.set('uppyPreAuthToken', this.preAuthToken)
|
searchParams.set('uppyPreAuthToken', this.preAuthToken)
|
||||||
|
}
|
||||||
|
if (authCallbackToken) {
|
||||||
|
searchParams.set('authCallbackToken', authCallbackToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${this.hostname}/${this.id}/connect?${params}`
|
return `${this.hostname}/${this.id}/connect?${searchParams}`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loginSimpleAuth({
|
protected async loginSimpleAuth({
|
||||||
|
|
@ -181,67 +189,62 @@ export default class Provider<M extends Meta, B extends Body>
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await this.ensurePreAuth()
|
await this.ensurePreAuth()
|
||||||
|
|
||||||
signal.throwIfAborted()
|
signal.throwIfAborted()
|
||||||
|
|
||||||
const link = this.authUrl({ query: { uppyVersions }, authFormData })
|
// Important: We need to do this synchronously, or else browsers might block the popup.
|
||||||
|
// (We cannot wait until the websocket connection is established).
|
||||||
|
// This opens up a race condtition if the websocket is not connected before the user
|
||||||
|
// completes(or cancels) authentication, but that’s a small compromose we gotta make.
|
||||||
|
const authCallbackToken = crypto.randomUUID()
|
||||||
|
|
||||||
|
const link = this.authUrl({
|
||||||
|
query: { uppyVersions },
|
||||||
|
authFormData,
|
||||||
|
authCallbackToken,
|
||||||
|
})
|
||||||
const authWindow = window.open(link, '_blank')
|
const authWindow = window.open(link, '_blank')
|
||||||
let interval: number | undefined
|
let interval: number | undefined
|
||||||
let handleMessage: ((e: MessageEvent<any>) => void) | undefined
|
let webSocket: WebSocket | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await new Promise((resolve, reject) => {
|
const host = getSocketHost(this.opts.companionUrl)
|
||||||
handleMessage = (e: MessageEvent<any>) => {
|
|
||||||
if (e.source !== authWindow) {
|
// Note that this promise is not guaranteed to settle in all cases
|
||||||
let jsonData = ''
|
const token = await new Promise<string>((resolve, reject) => {
|
||||||
try {
|
webSocket = new WebSocket(
|
||||||
// TODO improve our uppy logger so that it can take an arbitrary number of arguments,
|
`${host}/api2/auth-callback/token/${authCallbackToken}`,
|
||||||
// each either objects, errors or strings,
|
)
|
||||||
// then we don’t have to manually do these things like json stringify when logging.
|
|
||||||
// the logger should never throw an error.
|
webSocket.addEventListener('close', () => {
|
||||||
jsonData = JSON.stringify(e.data)
|
reject(new Error('Socket closed'))
|
||||||
} catch (_err) {
|
})
|
||||||
// in case JSON.stringify fails (ignored)
|
|
||||||
|
webSocket.addEventListener('error', (error) => {
|
||||||
|
this.uppy.log(
|
||||||
|
`Companion socket error ${JSON.stringify(error)}, closing socket`,
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
|
webSocket?.close() // 'close' event will be emitted
|
||||||
|
})
|
||||||
|
|
||||||
|
webSocket.addEventListener('message', (e) => {
|
||||||
|
try {
|
||||||
|
const { token, error } = JSON.parse(e.data)
|
||||||
|
if (error) {
|
||||||
|
reject(new Error('Authentication reported error'))
|
||||||
|
} else if (!token) {
|
||||||
|
reject(new Error('Authentication did not return a token'))
|
||||||
|
} else {
|
||||||
|
resolve(token)
|
||||||
}
|
}
|
||||||
this.uppy.log(
|
} catch (err) {
|
||||||
`ignoring event from unknown source ${jsonData}`,
|
reject(err)
|
||||||
'warning',
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const { companionAllowedHosts } = this.#getPlugin().opts
|
signal.addEventListener('abort', () =>
|
||||||
if (!isOriginAllowed(e.origin, companionAllowedHosts)) {
|
reject(new Error('Authentication was aborted')),
|
||||||
this.uppy.log(
|
)
|
||||||
`ignoring event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`,
|
|
||||||
'warning',
|
|
||||||
)
|
|
||||||
// We cannot reject here because the page might send events from other origins
|
|
||||||
// before sending the "real" auth completed event.
|
|
||||||
// for example Box has a "Pendo" tool that sends events to the opener
|
|
||||||
// https://github.com/transloadit/uppy/pull/5719
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a string before doing the JSON.parse to maintain support
|
|
||||||
// for older Companion versions that used object references
|
|
||||||
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
|
||||||
|
|
||||||
if (data.error) {
|
|
||||||
const { uppy } = this
|
|
||||||
const message = uppy.i18n('authAborted')
|
|
||||||
uppy.info({ message }, 'warning', 5000)
|
|
||||||
reject(new Error('auth aborted'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.token) {
|
|
||||||
reject(new Error('did not receive token from auth window'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(this.setAuthToken(data.token))
|
|
||||||
}
|
|
||||||
|
|
||||||
// poll for user closure of the window, so we can reject when it happens
|
// poll for user closure of the window, so we can reject when it happens
|
||||||
if (authWindow) {
|
if (authWindow) {
|
||||||
|
|
@ -251,15 +254,21 @@ export default class Provider<M extends Meta, B extends Body>
|
||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
signal.addEventListener('abort', () => reject(new Error('Aborted')))
|
|
||||||
window.addEventListener('message', handleMessage)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.setAuthToken(token)
|
||||||
|
} catch (err) {
|
||||||
|
const message = this.uppy.i18n('authAborted')
|
||||||
|
this.uppy.info({ message }, 'warning', 5000)
|
||||||
|
this.uppy.log(`Authentication failed: ${err.message}`, 'warning')
|
||||||
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
// cleanup:
|
// cleanup:
|
||||||
authWindow?.close()
|
// if we don't setTimeout, the window doesn't really close (I don't know why).
|
||||||
window.clearInterval(interval)
|
setTimeout(() => authWindow?.close(), 1)
|
||||||
if (handleMessage) window.removeEventListener('message', handleMessage)
|
this.uppy.log(`Closing auth callback socket ${authCallbackToken}`)
|
||||||
|
webSocket?.close()
|
||||||
|
clearInterval(interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,12 +234,13 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remote uploading consists of two steps:
|
* Remote uploading uses a single queue admission per attempt:
|
||||||
* 1. #requestSocketToken which starts the download/upload in companion and returns a unique token for the upload.
|
* 1. Acquire one queue slot.
|
||||||
* Then companion will halt the upload until:
|
* 2. Reuse an existing serverToken or request a new one from Companion.
|
||||||
* 2. #awaitRemoteFileUpload is called, which will open/ensure a websocket connection towards companion, with the
|
* 3. Open/maintain the websocket and wait for Companion to finish.
|
||||||
* previously generated token provided. It returns a promise that will resolve/reject once the file has finished
|
*
|
||||||
* uploading or is otherwise done (failed, canceled)
|
* This prevents socket tokens from being created long before their websocket
|
||||||
|
* session is admitted by the queue.
|
||||||
*/
|
*/
|
||||||
async uploadRemoteFile(
|
async uploadRemoteFile(
|
||||||
file: RemoteUppyFile<M, B>,
|
file: RemoteUppyFile<M, B>,
|
||||||
|
|
@ -248,72 +249,46 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { signal, getQueue } = options || {}
|
const { signal, getQueue } = options || {}
|
||||||
|
const queue = getQueue()
|
||||||
|
|
||||||
return await pRetry(
|
return await pRetry(
|
||||||
async () => {
|
async () => {
|
||||||
// if we already have a serverToken, assume that we are resuming the existing server upload id
|
const queueRemoteUploadAttempt = queue.wrapPromiseFunction(
|
||||||
const existingServerToken = this.uppy.getFile(file.id)?.serverToken
|
async () => {
|
||||||
if (existingServerToken != null) {
|
const currentFile = this.uppy.getFile(file.id) as
|
||||||
this.uppy.log(
|
| RemoteUppyFile<M, B>
|
||||||
`Connecting to exiting websocket ${existingServerToken}`,
|
| undefined
|
||||||
)
|
if (currentFile == null) return undefined
|
||||||
return this.#awaitRemoteFileUpload({
|
|
||||||
file,
|
|
||||||
queue: getQueue(),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const queueRequestSocketToken = getQueue().wrapPromiseFunction(
|
let serverToken = currentFile.serverToken
|
||||||
async (
|
|
||||||
...args: [
|
|
||||||
{
|
|
||||||
file: RemoteUppyFile<M, B>
|
|
||||||
postBody: Record<string, unknown>
|
|
||||||
signal: AbortSignal
|
|
||||||
},
|
|
||||||
]
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
return await this.#requestSocketToken(...args)
|
|
||||||
} catch (outerErr) {
|
|
||||||
// throwing AbortError will cause p-retry to stop retrying
|
|
||||||
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
|
||||||
|
|
||||||
if (outerErr.cause == null) throw outerErr
|
if (serverToken != null) {
|
||||||
const err = outerErr.cause
|
this.uppy.log(`Connecting to exiting websocket ${serverToken}`)
|
||||||
|
} else {
|
||||||
|
serverToken = await this.#requestSocketTokenWithRetryStrategy({
|
||||||
|
file: currentFile,
|
||||||
|
postBody: reqBody,
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
|
||||||
const isRetryableHttpError = () =>
|
if (!this.uppy.getFile(file.id)) return undefined
|
||||||
[408, 409, 429, 418, 423].includes(err.statusCode) ||
|
|
||||||
(err.statusCode >= 500 &&
|
|
||||||
err.statusCode <= 599 &&
|
|
||||||
![501, 505].includes(err.statusCode))
|
|
||||||
if (err.name === 'HttpError' && !isRetryableHttpError())
|
|
||||||
throw new AbortError(err)
|
|
||||||
|
|
||||||
// p-retry will retry most other errors,
|
this.uppy.setFileState(file.id, { serverToken })
|
||||||
// but it will not retry TypeError (except network error TypeErrors)
|
|
||||||
throw err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latestFile = this.uppy.getFile(file.id) as
|
||||||
|
| RemoteUppyFile<M, B>
|
||||||
|
| undefined
|
||||||
|
if (latestFile == null) return undefined
|
||||||
|
|
||||||
|
return this.#awaitRemoteFileUpload({
|
||||||
|
file: latestFile,
|
||||||
|
signal,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{ priority: -1 },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const serverToken = await queueRequestSocketToken({
|
return queueRemoteUploadAttempt().abortOn(signal)
|
||||||
file,
|
|
||||||
postBody: reqBody,
|
|
||||||
signal,
|
|
||||||
}).abortOn(signal)
|
|
||||||
|
|
||||||
if (!this.uppy.getFile(file.id)) return undefined // has file since been removed?
|
|
||||||
|
|
||||||
this.uppy.setFileState(file.id, { serverToken })
|
|
||||||
|
|
||||||
return this.#awaitRemoteFileUpload({
|
|
||||||
file: this.uppy.getFile(file.id) as RemoteUppyFile<M, B>, // re-fetching file because it might have changed in the meantime
|
|
||||||
queue: getQueue(),
|
|
||||||
signal,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
retries: retryCount,
|
retries: retryCount,
|
||||||
|
|
@ -360,6 +335,39 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
||||||
return res.token
|
return res.token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#requestSocketTokenWithRetryStrategy = async ({
|
||||||
|
file,
|
||||||
|
postBody,
|
||||||
|
signal,
|
||||||
|
}: {
|
||||||
|
file: RemoteUppyFile<M, B>
|
||||||
|
postBody: Record<string, unknown>
|
||||||
|
signal: AbortSignal
|
||||||
|
}): Promise<string> => {
|
||||||
|
try {
|
||||||
|
return await this.#requestSocketToken({ file, postBody, signal })
|
||||||
|
} catch (outerErr) {
|
||||||
|
// throwing AbortError will cause p-retry to stop retrying
|
||||||
|
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
||||||
|
|
||||||
|
if (outerErr.cause == null) throw outerErr
|
||||||
|
const err = outerErr.cause
|
||||||
|
|
||||||
|
const isRetryableHttpError = () =>
|
||||||
|
[408, 409, 429, 418, 423].includes(err.statusCode) ||
|
||||||
|
(err.statusCode >= 500 &&
|
||||||
|
err.statusCode <= 599 &&
|
||||||
|
![501, 505].includes(err.statusCode))
|
||||||
|
if (err.name === 'HttpError' && !isRetryableHttpError()) {
|
||||||
|
throw new AbortError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// p-retry will retry most other errors,
|
||||||
|
// but it will not retry TypeError (except network error TypeErrors)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method will ensure a websocket for the specified file and returns a promise that resolves
|
* This method will ensure a websocket for the specified file and returns a promise that resolves
|
||||||
* when the file has finished downloading, or rejects if it fails.
|
* when the file has finished downloading, or rejects if it fails.
|
||||||
|
|
@ -367,11 +375,9 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
||||||
*/
|
*/
|
||||||
async #awaitRemoteFileUpload({
|
async #awaitRemoteFileUpload({
|
||||||
file,
|
file,
|
||||||
queue,
|
|
||||||
signal,
|
signal,
|
||||||
}: {
|
}: {
|
||||||
file: RemoteUppyFile<M, B>
|
file: RemoteUppyFile<M, B>
|
||||||
queue: any
|
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
let removeEventHandlers: () => void
|
let removeEventHandlers: () => void
|
||||||
|
|
@ -430,126 +436,113 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
||||||
function resetActivityTimeout() {
|
function resetActivityTimeout() {
|
||||||
clearTimeout(activityTimeout)
|
clearTimeout(activityTimeout)
|
||||||
if (isPaused) return
|
if (isPaused) return
|
||||||
activityTimeout = setTimeout(
|
activityTimeout = setTimeout(() => {
|
||||||
() =>
|
onFatalError(
|
||||||
onFatalError(
|
new Error('Timeout waiting for message from Companion socket'),
|
||||||
new Error(
|
)
|
||||||
'Timeout waiting for message from Companion socket',
|
}, socketActivityTimeoutMs)
|
||||||
),
|
|
||||||
),
|
|
||||||
socketActivityTimeoutMs,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queue
|
const reconnectWebsocket = async () =>
|
||||||
.wrapPromiseFunction(async () => {
|
new Promise((_, rejectSocket) => {
|
||||||
const reconnectWebsocket = async () =>
|
socket = new WebSocket(`${host}/api/${token}`)
|
||||||
new Promise((_, rejectSocket) => {
|
|
||||||
socket = new WebSocket(`${host}/api/${token}`)
|
|
||||||
|
|
||||||
resetActivityTimeout()
|
resetActivityTimeout()
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
socket = undefined
|
socket = undefined
|
||||||
rejectSocket(new Error('Socket closed unexpectedly'))
|
rejectSocket(new Error('Socket closed unexpectedly'))
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener('error', (error) => {
|
|
||||||
this.uppy.log(
|
|
||||||
`Companion socket error ${JSON.stringify(
|
|
||||||
error,
|
|
||||||
)}, closing socket`,
|
|
||||||
'warning',
|
|
||||||
)
|
|
||||||
socket?.close() // will 'close' event to be emitted
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
|
||||||
sendState()
|
|
||||||
})
|
|
||||||
|
|
||||||
socket.addEventListener('message', (e) => {
|
|
||||||
resetActivityTimeout()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { action, payload } = JSON.parse(e.data)
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'progress': {
|
|
||||||
emitSocketProgress(
|
|
||||||
this,
|
|
||||||
payload,
|
|
||||||
this.uppy.getFile(file.id),
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'success': {
|
|
||||||
// payload.response is sent from companion for xhr-upload (aka uploadMultipart in companion) and
|
|
||||||
// s3 multipart (aka uploadS3Multipart)
|
|
||||||
// but not for tus/transloadit (aka uploadTus)
|
|
||||||
// responseText is a string which may or may not be in JSON format
|
|
||||||
// this means that an upload destination of xhr or s3 multipart MUST respond with valid JSON
|
|
||||||
// to companion, or the JSON.parse will crash
|
|
||||||
const text = payload.response?.responseText
|
|
||||||
|
|
||||||
this.uppy.emit(
|
|
||||||
'upload-success',
|
|
||||||
this.uppy.getFile(file.id),
|
|
||||||
{
|
|
||||||
uploadURL: payload.url,
|
|
||||||
status: payload.response?.status ?? 200,
|
|
||||||
body: text
|
|
||||||
? (JSON.parse(text) as B)
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
socketAbortController?.abort?.()
|
|
||||||
resolve()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'error': {
|
|
||||||
const { message } = payload.error
|
|
||||||
throw Object.assign(new Error(message), {
|
|
||||||
cause: payload.error,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
this.uppy.log(
|
|
||||||
`Companion socket unknown action ${action}`,
|
|
||||||
'warning',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
onFatalError(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeSocket = () => {
|
|
||||||
this.uppy.log(`Closing socket ${file.id}`)
|
|
||||||
clearTimeout(activityTimeout)
|
|
||||||
if (socket) socket.close()
|
|
||||||
socket = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
socketAbortController.signal.addEventListener(
|
|
||||||
'abort',
|
|
||||||
() => {
|
|
||||||
closeSocket()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
await pRetry(reconnectWebsocket, {
|
|
||||||
retries: retryCount,
|
|
||||||
signal: socketAbortController.signal,
|
|
||||||
onFailedAttempt: () => {
|
|
||||||
if (socketAbortController.signal.aborted) return // don't log in this case
|
|
||||||
this.uppy.log(`Retrying websocket ${file.id}`)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
})()
|
|
||||||
.abortOn(socketAbortController.signal)
|
socket.addEventListener('error', (error) => {
|
||||||
|
this.uppy.log(
|
||||||
|
`Companion socket error ${JSON.stringify(
|
||||||
|
error,
|
||||||
|
)}, closing socket`,
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
|
socket?.close() // will 'close' event to be emitted
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.addEventListener('open', () => {
|
||||||
|
sendState()
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.addEventListener('message', (e) => {
|
||||||
|
resetActivityTimeout()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { action, payload } = JSON.parse(e.data)
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'progress': {
|
||||||
|
emitSocketProgress(
|
||||||
|
this,
|
||||||
|
payload,
|
||||||
|
this.uppy.getFile(file.id),
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'success': {
|
||||||
|
// payload.response is sent from companion for xhr-upload (aka uploadMultipart in companion) and
|
||||||
|
// s3 multipart (aka uploadS3Multipart)
|
||||||
|
// but not for tus/transloadit (aka uploadTus)
|
||||||
|
// responseText is a string which may or may not be in JSON format
|
||||||
|
// this means that an upload destination of xhr or s3 multipart MUST respond with valid JSON
|
||||||
|
// to companion, or the JSON.parse will crash
|
||||||
|
const text = payload.response?.responseText
|
||||||
|
|
||||||
|
this.uppy.emit(
|
||||||
|
'upload-success',
|
||||||
|
this.uppy.getFile(file.id),
|
||||||
|
{
|
||||||
|
uploadURL: payload.url,
|
||||||
|
status: payload.response?.status ?? 200,
|
||||||
|
body: text ? (JSON.parse(text) as B) : undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
socketAbortController?.abort?.()
|
||||||
|
resolve()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
const { message } = payload.error
|
||||||
|
throw Object.assign(new Error(message), {
|
||||||
|
cause: payload.error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
this.uppy.log(
|
||||||
|
`Companion socket unknown action ${action}`,
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
onFatalError(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const closeSocket = () => {
|
||||||
|
this.uppy.log(`Closing socket ${file.id}`)
|
||||||
|
clearTimeout(activityTimeout)
|
||||||
|
if (socket) socket.close()
|
||||||
|
socket = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
socketAbortController.signal.addEventListener('abort', () => {
|
||||||
|
closeSocket()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await pRetry(reconnectWebsocket, {
|
||||||
|
retries: retryCount,
|
||||||
|
signal: socketAbortController.signal,
|
||||||
|
onFailedAttempt: () => {
|
||||||
|
if (socketAbortController.signal.aborted) return // don't log in this case
|
||||||
|
this.uppy.log(`Retrying websocket ${file.id}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (socketAbortController.signal.aborted) return
|
if (socketAbortController.signal.aborted) return
|
||||||
onFatalError(err)
|
onFatalError(err)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import getAllowedHosts, { isOriginAllowed } from './getAllowedHosts.js'
|
import getAllowedHosts from './getAllowedHosts.js'
|
||||||
|
|
||||||
describe('getAllowedHosts', () => {
|
describe('getAllowedHosts', () => {
|
||||||
it('can convert companionAllowedHosts', () => {
|
it('can convert companionAllowedHosts', () => {
|
||||||
|
|
@ -30,20 +30,3 @@ describe('getAllowedHosts', () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isOriginAllowed', () => {
|
|
||||||
it('should check origin', () => {
|
|
||||||
expect(isOriginAllowed('a', [/^.+$/])).toBeTruthy()
|
|
||||||
expect(isOriginAllowed('a', ['^.+$'])).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
isOriginAllowed('www.transloadit.com', ['^www\\.transloadit\\.com$']),
|
|
||||||
).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
isOriginAllowed('www.transloadit.com', ['^transloadit\\.com$']),
|
|
||||||
).toBeFalsy()
|
|
||||||
expect(isOriginAllowed('match', ['fail', 'match'])).toBeTruthy()
|
|
||||||
expect(
|
|
||||||
isOriginAllowed('www.transloadit.com', ['\\.transloadit\\.com$']),
|
|
||||||
).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -47,15 +47,3 @@ export default function getAllowedHosts(
|
||||||
ret = escapeRegex(ret)
|
ret = escapeRegex(ret)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOriginAllowed(
|
|
||||||
origin: string,
|
|
||||||
allowedOrigin: string | RegExp | Array<string | RegExp> | undefined,
|
|
||||||
): boolean {
|
|
||||||
const patterns = Array.isArray(allowedOrigin)
|
|
||||||
? allowedOrigin.map(wrapInRegex)
|
|
||||||
: [wrapInRegex(allowedOrigin)]
|
|
||||||
return patterns.some(
|
|
||||||
(pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`),
|
|
||||||
) // allowing for trailing '/'
|
|
||||||
}
|
|
||||||
|
|
|
||||||
2
packages/@uppy/companion/.gitignore
vendored
2
packages/@uppy/companion/.gitignore
vendored
|
|
@ -34,6 +34,6 @@ test/output/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Transpiled
|
# Transpiled
|
||||||
./lib/
|
./dist/
|
||||||
infra/kube/companion/uppy-env.yaml
|
infra/kube/companion/uppy-env.yaml
|
||||||
scripts/.tl-deploy-hosts-danger.txt
|
scripts/.tl-deploy-hosts-danger.txt
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
# @uppy/companion
|
# @uppy/companion
|
||||||
|
|
||||||
|
## 6.2.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 49db42d: Fix bug with 429 not returning JSON response with message
|
||||||
|
- 4652dc6: upgrade @aws-sdk/ deps in @uppy/companion
|
||||||
|
|
||||||
## 6.2.1
|
## 6.2.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ data:
|
||||||
COMPANION_BOX_SECRET: 'YOUR BOX SECRET'
|
COMPANION_BOX_SECRET: 'YOUR BOX SECRET'
|
||||||
COMPANION_GOOGLE_KEY: 'YOUR GOOGLE KEY'
|
COMPANION_GOOGLE_KEY: 'YOUR GOOGLE KEY'
|
||||||
COMPANION_GOOGLE_SECRET: 'YOUR GOOGLE SECRET'
|
COMPANION_GOOGLE_SECRET: 'YOUR GOOGLE SECRET'
|
||||||
COMPANION_INSTAGRAM_KEY: 'YOUR INSTAGRAM KEY'
|
|
||||||
COMPANION_INSTAGRAM_SECRET: 'YOUR INSTAGRAM SECRET'
|
|
||||||
COMPANION_AWS_KEY: 'YOUR AWS KEY'
|
COMPANION_AWS_KEY: 'YOUR AWS KEY'
|
||||||
COMPANION_AWS_SECRET: 'YOUR AWS SECRET'
|
COMPANION_AWS_SECRET: 'YOUR AWS SECRET'
|
||||||
COMPANION_AWS_BUCKET: 'YOUR AWS S3 BUCKET'
|
COMPANION_AWS_BUCKET: 'YOUR AWS S3 BUCKET'
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ Companion is a server integration for
|
||||||
[Uppy](https://github.com/transloadit/uppy) file uploader.
|
[Uppy](https://github.com/transloadit/uppy) file uploader.
|
||||||
|
|
||||||
It handles the server-to-server communication between your server and file
|
It handles the server-to-server communication between your server and file
|
||||||
storage providers such as Google Drive, Dropbox, Instagram, etc. **Companion is
|
storage providers such as Google Drive, Dropbox, etc. **Companion is
|
||||||
not a target to upload files to**. For this, use a <https://tus.io> server (if
|
not a target to upload files to**. For this, use a <https://tus.io> server (if
|
||||||
you want resumable) or your existing Apache/Nginx server (if you don’t).
|
you want resumable) or your existing Apache/Nginx server (if you don’t).
|
||||||
[See here for full documentation](https://uppy.io/docs/companion/)
|
[See here for full documentation](https://uppy.io/docs/companion/)
|
||||||
|
|
@ -68,7 +68,10 @@ on, you call the `socket` method like so.
|
||||||
// ...
|
// ...
|
||||||
const server = app.listen(PORT)
|
const server = app.listen(PORT)
|
||||||
|
|
||||||
companion.socket(server)
|
// Pass the same `options` object you passed to `companion.app(options)` —
|
||||||
|
// `companion.socket` needs `options.server` to compute the external base path
|
||||||
|
// for incoming WS URLs (important behind reverse proxies).
|
||||||
|
companion.socket(server, options)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run as standalone server
|
### Run as standalone server
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,14 @@ class Gauge {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const middleware = (req, res, next) => {
|
type Req = { url?: string }
|
||||||
|
type Res = {
|
||||||
|
setHeader: (key: string, value: string) => void
|
||||||
|
end: (s?: string) => void
|
||||||
|
}
|
||||||
|
type Next = () => void
|
||||||
|
|
||||||
|
const middleware = (req: Req, res: Res, next: Next) => {
|
||||||
// simulate prometheus metrics endpoint:
|
// simulate prometheus metrics endpoint:
|
||||||
if (req.url === '/metrics') {
|
if (req.url === '/metrics') {
|
||||||
res.setHeader('Content-Type', 'text/plain')
|
res.setHeader('Content-Type', 'text/plain')
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
export class Upload {
|
|
||||||
constructor(file, options) {
|
|
||||||
this.url = 'https://tus.endpoint/files/foo-bar'
|
|
||||||
this.options = options
|
|
||||||
}
|
|
||||||
|
|
||||||
_triggerProgressThenSuccess() {
|
|
||||||
this.options.onProgress(this.options.uploadSize, this.options.uploadSize)
|
|
||||||
setTimeout(() => this.options.onSuccess(), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
setTimeout(this._triggerProgressThenSuccess.bind(this), 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
abort() {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default { Upload }
|
|
||||||
42
packages/@uppy/companion/__mocks__/tus-js-client.ts
Normal file
42
packages/@uppy/companion/__mocks__/tus-js-client.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
type UploadOptions = {
|
||||||
|
uploadSize: number
|
||||||
|
onProgress: (bytesUploaded: number, bytesTotal: number) => void
|
||||||
|
onSuccess: () => void
|
||||||
|
} & Record<string, unknown>
|
||||||
|
|
||||||
|
let lastUploadFile: unknown
|
||||||
|
|
||||||
|
export function __getLastUploadFile(): unknown {
|
||||||
|
return lastUploadFile
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __resetTusMockState(): void {
|
||||||
|
lastUploadFile = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Upload {
|
||||||
|
url: string
|
||||||
|
|
||||||
|
options: UploadOptions
|
||||||
|
|
||||||
|
constructor(file: unknown, options: UploadOptions) {
|
||||||
|
lastUploadFile = file
|
||||||
|
this.url = 'https://tus.endpoint/files/foo-bar'
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
_triggerProgressThenSuccess() {
|
||||||
|
this.options.onProgress(this.options.uploadSize, this.options.uploadSize)
|
||||||
|
setTimeout(() => this.options.onSuccess(), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
setTimeout(this._triggerProgressThenSuccess.bind(this), 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { Upload }
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import '../lib/standalone/start-server.js'
|
|
||||||
|
|
@ -26,10 +26,6 @@ COMPANION_GOOGLE_KEY=
|
||||||
COMPANION_GOOGLE_SECRET=
|
COMPANION_GOOGLE_SECRET=
|
||||||
COMPANION_GOOGLE_SECRET_FILE=
|
COMPANION_GOOGLE_SECRET_FILE=
|
||||||
|
|
||||||
COMPANION_INSTAGRAM_KEY=
|
|
||||||
COMPANION_INSTAGRAM_SECRET=
|
|
||||||
COMPANION_INSTAGRAM_SECRET_FILE=
|
|
||||||
|
|
||||||
COMPANION_FACEBOOK_KEY=
|
COMPANION_FACEBOOK_KEY=
|
||||||
COMPANION_FACEBOOK_SECRET=
|
COMPANION_FACEBOOK_SECRET=
|
||||||
COMPANION_FACEBOOK_SECRET_FILE=
|
COMPANION_FACEBOOK_SECRET_FILE=
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue