diff --git a/BUNDLE-README.md b/BUNDLE-README.md index 40f6b71940..add956bcc7 100644 --- a/BUNDLE-README.md +++ b/BUNDLE-README.md @@ -2,7 +2,7 @@ Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use this from a CDN -(``) +(``) or bundle it with your webapp. Note that the recommended way to use Uppy is to install it with yarn/npm and use diff --git a/CHANGELOG.md b/CHANGELOG.md index e81dc5ff45..b6028f9dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,23 @@ Please add your entries in this format: In the current stage we aim to release a new version at least every month. +## 4.0.0-beta.11 + +Released: 2024-06-11 + +| Package | Version | Package | Version | +| -------------------- | ------------- | -------------------- | ------------- | +| @uppy/aws-s3 | 4.0.0-beta.6 | @uppy/react | 4.0.0-beta.6 | +| @uppy/locales | 4.0.0-beta.3 | @uppy/transloadit | 4.0.0-beta.8 | +| @uppy/provider-views | 4.0.0-beta.8 | uppy | 4.0.0-beta.11 | + +- docs: clarify assemblyOptions for @uppy/transloadit (Merlijn Vos / #5226) +- @uppy/react: remove `react:` prefix from `id` & allow `id` as a prop (Merlijn Vos / #5228) +- docs: correct allowedMetaFields (Merlijn Vos / #5227) +- docs: remove `extraData` note from migration guide (Mikael Finstad / #5219) +- meta: fix AWS test suite (Antoine du Hamel / #5229) + + ## 4.0.0-beta.10 Released: 2024-06-04 @@ -420,6 +437,21 @@ Released: 2024-03-28 - @uppy/vue: [v4.x] remove manual types (Antoine du Hamel / #4803) - meta: prepare release workflow for beta versions (Antoine du Hamel) +## 3.26.1 + +Released: 2024-06-11 + +| Package | Version | Package | Version | +| -------------------- | ------- | -------------------- | ------- | +| @uppy/locales | 3.5.4 | @uppy/transloadit | 3.7.1 | +| @uppy/provider-views | 3.12.1 | uppy | 3.26.1 | + +- meta: Improve aws-node example readme (Artur Paikin / #4753) +- @uppy/locales: Added translation string (it_IT) (Samuel / #5237) +- @uppy/transloadit: fix transloadit:result event (Merlijn Vos / #5231) +- @uppy/provider-views: fix wrong font for files (Merlijn Vos / #5234) + + ## 3.26.0 Released: 2024-06-04 diff --git a/README.md b/README.md index de2a387000..2f039b9820 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus ``` Add CSS -[uppy.min.css](https://releases.transloadit.com/uppy/v4.0.0-beta.10/uppy.min.css), +[uppy.min.css](https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.css), either to your HTML page’s `` or include in JS, if your bundler of choice supports it. @@ -94,7 +94,7 @@ object. ```html @@ -105,7 +105,7 @@ object. Uppy, Dashboard, Tus, - } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.10/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs' const uppy = new Uppy() uppy.use(Dashboard, { target: '#files-drag-drop' }) diff --git a/docs/companion.md b/docs/companion.md index 742c25458a..6a8fffbf1e 100644 --- a/docs/companion.md +++ b/docs/companion.md @@ -283,7 +283,7 @@ const options = { endpoint: 'https://{service}.{region}.amazonaws.com', conditions: [], useAccelerateEndpoint: false, - getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`, + getKey: ({ filename }) => `${crypto.randomUUID()}-${filename}`, expires: 800, // seconds }, allowLocalUrls: false, @@ -473,13 +473,14 @@ from the AWS SDK. The name of the bucket to store uploaded files in. -It can be function that returns the name of the bucket as a `string` and takes -the following arguments: +A `string` or function that returns the name of the bucket as a `string` and +takes one argument which is an object with the following properties: -- [`http.IncomingMessage`][], the HTTP request (will be `null` for remote - uploads) -- metadata provided by the user for the file (will be `undefined` for local - uploads) +- `filename`, the original name of the uploaded file; +- `metadata` provided by the user for the file (will only be provided during the + initial calls for each uploaded files, otherwise it will be `undefined`). +- `req`, Express.js `Request` object. Do not use any Companion internals from + the req object, as these might change in any minor version of Companion. ##### `s3.region` `COMPANION_AWS_REGION` @@ -508,18 +509,16 @@ expected, please [open an issue on the Uppy repository](https://github.com/transloadit/uppy/issues/new) so we can document it here. -##### `s3.getKey(req, filename, metadata)` +##### `s3.getKey({ filename, metadata, req })` Get the key name for a file. The key is the file path to which the file will be uploaded in your bucket. This option should be a function receiving three arguments: -- `req` [`http.IncomingMessage`][], the HTTP request, for _regular_ S3 uploads - using the `@uppy/aws-s3` plugin. This parameter is _not_ available for - multipart uploads using the `@uppy/aws-s3` or `@uppy/aws-s3-multipart` - plugins. This parameter is `null` for remote uploads. - `filename`, the original name of the uploaded file; - `metadata`, user-provided metadata for the file. +- `req`, Express.js `Request` object. Do not use any Companion internals from + the req object, as these might change in any minor version of Companion. This function should return a string `key`. The `req` parameter can be used to upload to a user-specific folder in your bucket, for example: @@ -530,7 +529,7 @@ app.use( uppy.app({ providerOptions: { s3: { - getKey: (req, filename, metadata) => `${req.user.id}/${filename}`, + getKey: ({ req, filename, metadata }) => `${req.user.id}/${filename}`, /* auth options */ }, }, @@ -546,7 +545,7 @@ app.use( uppy.app({ providerOptions: { s3: { - getKey: (req, filename, metadata) => filename, + getKey: ({ filename, metadata }) => filename, }, }, }), @@ -909,8 +908,6 @@ This would get the Companion instance running on `http://localhost:3020`. It uses [`node --watch`](https://nodejs.org/api/cli.html#--watch) so it will automatically restart when files are changed. -[`http.incomingmessage`]: - https://nodejs.org/api/http.html#class-httpincomingmessage [box]: /docs/box [dropbox]: /docs/dropbox [facebook]: /docs/facebook diff --git a/docs/guides/migration-guides.md b/docs/guides/migration-guides.md index 34641255b0..03694fa5ba 100644 --- a/docs/guides/migration-guides.md +++ b/docs/guides/migration-guides.md @@ -2,16 +2,19 @@ These cover all the major Uppy versions and how to migrate to them. -## Migrate from Uppy 3.x to 4.x - -### Companion +## Migrate from Companion 4.x to 5.x +- Node.js `>=18.20.0` is now required. - `COMPANION_REDIS_EXPRESS_SESSION_PREFIX` now defaults to `companion-session:` (before `sess:`). To revert keep backwards compatibility, set the environment variable `COMPANION_REDIS_EXPRESS_SESSION_PREFIX=sess:`. - The URL endpoint (used by the `Url`/`Link` plugin) is now turned off by default and must be explicitly enabled with `COMPANION_ENABLE_URL_ENDPOINT=true` or `enableUrlEndpoint: true`. +- The option `getKey(req, filename, metadata)` has changed signature to + `getKey({ filename, metadata, req })`. +- The option `bucket(req, metadata)` has changed signature to + `bucketOrFn({ req, metadata, filename })`. - Custom provider breaking changes. If you have not implemented a custom provider, you should not be affected. - The static `getExtraConfig` property has been renamed to @@ -19,9 +22,6 @@ These cover all the major Uppy versions and how to migrate to them. - The static `authProvider` property has been renamed to `oauthProvider`. - Endpoint `GET /s3/params` now returns `{ method: "POST" }` instead of `{ method: "post" }`. This will not affect most people. -- The Companion [`error` event](https://uppy.io/docs/companion/#events) now no - longer includes `extraData` inside the `payload.error` property. `extraData` - is (and was also before) included in the `payload`. - `access-control-allow-headers` is no longer included in `Access-Control-Expose-Headers`, and `uppy-versions` is no longer an allowed header. We are not aware of any issues this might cause. @@ -35,6 +35,14 @@ These cover all the major Uppy versions and how to migrate to them. ### `@uppy/companion-client` +:::info + +Unless you built a custom provider, you don’t use `@uppy/companion-client` +directly but through provider plugins such as `@uppy/google-drive`. In which +case you don’t have to do anything. + +::: + - `supportsRefreshToken` now defaults to `false` instead of `true`. If you have implemented a custom provider, this might affect you. - `Socket` class is no longer in use and has been removed. Unless you used this @@ -45,6 +53,158 @@ These cover all the major Uppy versions and how to migrate to them. the third argument. Instead, pass `{ skipPostResponse: true | false }`. This won’t affect you unless you’ve been using `RequestClient`. +## Migrate from Uppy 3.x to 4.x + +### TypeScript rewrite + +Almost all plugins have been completely rewritten in TypeScript! This means you +may run into type error all over the place, but the types now accurately show +Uppy’s state and files. + +There are too many small changes to cover, so you have to upgrade and see where +TypeScript complains. + +One important thing to note are the new generics on `@uppy/core`. + + + +```ts +import Uppy from '@uppy/core'; +// xhr-upload is for uploading to your own backend. +import XHRUpload from '@uppy/xhr-upload'; + +// Your own metadata on files +type Meta = { myCustomMetadata: string }; +// The response from your server +type Body = { someThingMyBackendReturns: string }; + +const uppy = new Uppy().use(XHRUpload, { + endpoint: '/upload', +}); + +const id = uppy.addFile({ + name: 'example.jpg', + data: new Blob(), + meta: { myCustomMetadata: 'foo' }, +}); + +// This is now typed +const { myCustomMetadata } = uppy.getFile(id).meta; + +await uppy.upload(); + +// This is strictly typed too +const { someThingMyBackendReturns } = uppy.getFile(id).response; +``` + +### `@uppy/aws-s3` and `@uppy/aws-s3-multipart` + +- `@uppy/aws-s3` and `@uppy/aws-s3-multipart` have been combined into a single + plugin. You should now only use `@uppy/aws-s3` with the new option, + [`shouldUseMultipart()`](/docs/aws-s3-multipart/#shouldusemultipartfile), to + allow you to switch between regular and multipart uploads. You can read more + about this in the + [plugin docs](https://uppy.io/docs/aws-s3-multipart/#when-should-i-use-it). +- Remove deprecated `prepareUploadParts` option. +- Companion’s options (`companionUrl`, `companionHeaders`, and + `companionCookieRules`) are renamed to more generic names (`endpoint`, + `headers`, and `cookieRules`). + + Using Companion with the `@uppy/aws-s3` plugin only makes sense if you already + need Companion for remote providers (such as Google Drive). When using your + own backend, you can let Uppy do all the heavy lifting on the client which it + would normally do for Companion, so you don’t have to implement that yourself. + + As long as you return the JSON for the expected endpoints (see our + [server example](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/index.js)), + you only need to set `endpoint`. + + If you are using Companion, rename the options. If you have a lot of + client-side complexity (`createMultipartUpload`, `signPart`, etc), consider + letting Uppy do this for you. + +### `@uppy/core` + +- The `close()` method has been renamed to `destroy()` to more accurately + reflect you can not recover from it without creating a new `Uppy` instance. +- The `clearUploadedFiles()` method has been renamed to `clear()` as a + convenience method to clear all the state. This can be useful when a user + navigates away and you want to clear the state on success. +- `bytesUploaded`, in `file.progress.bytesUploaded`, is now always a `boolean`, + instead of a `boolean` or `number`. + +### `@uppy/xhr-upload` + +Before the plugin had the options `getResponseData`, `getResponseError`, +`validateStatus` and `responseUrlFieldName`. These were inflexible and too +specific. Now we have hooks similar to `@uppy/tus`: + +- `onBeforeRequest` to manipulate the request before it is sent. +- `shouldRetry` to determine if a request should be retried. By default 3 + retries with exponential backoff. After three attempts it will throw an error, + regardless of whether you returned `true`. +- `onAfterResponse` called after a successful response but before Uppy resolves + the upload. + +Checkout the [docs](/docs/xhr-upload/) for more info. + +### `@uppy/transloadit` + +The options `signature`, `params`, `fields`, and `getAssemblyOptions` have been +removed in favor of [`assemblyOptions`](/docs/transloadit/#assemblyoptions), +which can be an object or an (async) function returning an object. + +When using `assemblyOptions()` as a function, it is now called only once for all +files, instead of per file. Before `@uppy/transloadit` was trying to be too +smart, creating multiple assemblies in which each assembly has files with +identical `fields`. This was done so you can use `fields` dynamically in your +template per file, instead of per assembly. + +Now we sent all metadata of a file inside the tus upload (which +`@uppy/transloadit` uses under the hood) and make it accessible in your +Transloadit template as `file_user_meta`. You should use `fields` for global +values in your template and `file_user_meta` for per file values. + +Another benefit of running `assemblyOptions()` only once, is that when +generating a +[secret](https://transloadit.com/docs/topics/signature-authentication/) on your +server (which you should), a network request is made only once for all files, +instead of per file. + +### CDN + +- We no longer build and serve the legacy build, made for IE11, on our CDN. + +### Miscellaneous + +- All uploaders plugins now consistently use + [`allowedMetaFields`](/docs/xhr-upload/#allowedmetafields). Before there were + inconsistencies between plugins. +- All plugin `titles` (what you see in the Dashboard when you open a plugin) are + now set from the `locale` option. See the + [docs](/docs/locales/#overriding-locale-strings-for-a-specific-plugin) on how + to overide a string. + +### `@uppy/angular` + +- Upgrade to Angular 18.x (17.x is still supported too) and to TS 5.4 + +### `@uppy/react` + +- Remove deprecated `useUppy` & reintroduce [`useUppyState`](docs/react/#hooks) +- You can no longer set `inline` on the `Dashboard` component, use `Dashboard` + or `DashboardModal` components respectively. + +### `@uppy/svelte` + +- Make Svelte 5 the peer dependency +- Remove UMD output + +### `@uppy/vue` + +- Migrate to Composition API with TypeScript & drop Vue 2 support +- Drop Vue 2 support + ## Migrate from Robodog to Uppy plugins Uppy is flexible and extensible through plugins. But the integration code could diff --git a/docs/uploader/aws-s3-multipart.mdx b/docs/uploader/aws-s3-multipart.mdx index 9154926f69..a21183ac72 100644 --- a/docs/uploader/aws-s3-multipart.mdx +++ b/docs/uploader/aws-s3-multipart.mdx @@ -166,7 +166,7 @@ import '@uppy/dashboard/dist/style.min.css'; const uppy = new Uppy() .use(Dashboard, { inline: true, target: 'body' }) .use(AwsS3, { - companionUrl: 'https://companion.uppy.io', + endpoint: 'https://companion.uppy.io', }); ``` @@ -215,22 +215,23 @@ uploaded. ::: -#### `companionUrl` +#### `endpoint` -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). +URL to your backend or to [Companion](/docs/companion) (`string`, default: +`null`). -#### `companionHeaders` +#### `headers` -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). +Custom headers that should be sent along to the [`endpoint`](#endpoint) on every +request (`Object`, default: `{}`). -#### `companionCookiesRule` +#### `cookiesRule` This option correlates to the [RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) (`string`, default: `'same-origin'`). -This tells the plugin whether to send cookies to [Companion](/docs/companion). +This tells the plugin whether to send cookies to the [`endpoint`](#endpoint). #### `retryDelays` @@ -386,9 +387,9 @@ The default implementation calls out to Companion’s S3 signing endpoints. Pass an array of field names to limit the metadata fields that will be added to upload as query parameters. -- Set this to `['name']` to only send the `name` field. -- Set this to `null` (the default) to send _all_ metadata fields. -- Set this to an empty array `[]` to not send any fields. +- Set it to `false` to not send any fields (or an empty array). +- Set it to `['name']` to only send the `name` field. +- Set it to `true` (the default) to send _all_ metadata fields.
Deprecated options @@ -432,7 +433,7 @@ upload sources), you can pass a boolean: ```js uppy.use(AwsS3, { // This is an example using Companion: - companionUrl: 'http://companion.uppy.io', + endpoint: 'http://companion.uppy.io', getTemporarySecurityCredentials: true, shouldUseMultipart: (file) => file.size > 100 * 2 ** 20, }); diff --git a/docs/uploader/transloadit.mdx b/docs/uploader/transloadit.mdx index 064856aaa4..4464ca25a3 100644 --- a/docs/uploader/transloadit.mdx +++ b/docs/uploader/transloadit.mdx @@ -100,8 +100,8 @@ uppy.on('transloadit:complete', (assembly) => {}); :::note -All [Transloadit plans](https://transloadit/pricing/) come with a hosted version -of Companion. +All [Transloadit plans](https://transloadit.com/pricing/) come with a hosted +version of Companion. ::: @@ -224,43 +224,27 @@ The object you can pass or return from a function has this structure: be [used dynamically in your template](https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions). -
- Examples +:::info -**As a function** +All your files end up in a single assembly and your `fields` are available +globally in your template. The metadata in your Uppy files is also sent along so +you can do things dynamically per file with `file.user_meta` in your template. -A custom `assemblyOptions()` option should return an object or a promise for an -object. +::: -```js -uppy.use(Transloadit, { - assemblyOptions(file) { - return { - params: { - auth: { key: 'TRANSLOADIT_AUTH_KEY_HERE' }, - template_id: 'xyz', - }, - fields: { - caption: file.meta.caption, - }, - }; - }, -}); -``` +
+ Examples -The `${fields.caption}` variable will be available in the Assembly spawned from -Template `xyz`. You can use this to dynamically watermark images for example. +**As a function** -`assemblyOptions()` may also return a Promise, so it could retrieve signed -Assembly parameters from a server. For example, assuming an endpoint -`/transloadit-params` that responds with a JSON object with -`{ params, signature }` properties: +Most likely you want to use a function to call your backend to generate a +signature and return your configuration. ```js uppy.use(Transloadit, { async assemblyOptions(file) { const res = await fetch('/transloadit-params'); - return response.json(); + return res.json(); }, }); ``` @@ -287,17 +271,10 @@ pass user input from a `
` to a Transloadit Assembly: // This will add form field values to each file's `.meta` object: uppy.use(Form, { getMetaFromForm: true }); uppy.use(Transloadit, { - getAssemblyOptions(file) { - return { - params: { - /* ... */ - }, - // Pass through the fields you need: - fields: { - message: file.meta.message, - }, - }; - }, + async assemblyOptions() { + const res = await fetch('/transloadit-params'); + return res.json(); + }; }); ``` @@ -379,7 +356,8 @@ uppy.use(Transloadit, { ``` Tranloadit will download the files and expose them to your Template as -`:original`, as if they were directly uploaded from the Uppy client. +`:original`, as if they were directly uploaded from the Uppy client. + :::note For this to work, the upload plugin must assign a publicly accessible diff --git a/docs/uploader/tus.mdx b/docs/uploader/tus.mdx index a998f19a42..15270ba6d7 100644 --- a/docs/uploader/tus.mdx +++ b/docs/uploader/tus.mdx @@ -215,9 +215,9 @@ uploads as [Tus Metadata](https://tus.io/protocols/resumable-upload.html#upload-metadata) (`Array`, default: `null`). -- Set this to `['name']` to only send the `name` field. -- Set this to `null` (the default) to send _all_ metadata fields. -- Set this to an empty array `[]` to not send any fields. +- Set it to `false` to not send any fields (or an empty array). +- Set it to `['name']` to only send the `name` field. +- Set it to `true` (the default) to send _all_ metadata fields. #### `limit` diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 5f4a2813df..a38f29f875 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -111,9 +111,9 @@ defaults to `'file'`. Pass an array of field names to limit the metadata fields that will be added to upload. -- Set this to an empty array `[]` to not send any fields. -- Set this to `['name']` to only send the `name` field. -- Set this to `null` (the default) to send _all_ metadata fields. +- Set it to `false` to not send any fields (or an empty array). +- Set it to `['name']` to only send the `name` field. +- Set it to `true` (the default) to send _all_ metadata fields. If the [`formData`](#formData-true) option is set to false, `metaFields` is ignored. diff --git a/e2e/clients/dashboard-aws-multipart/app.js b/e2e/clients/dashboard-aws-multipart/app.js index d4a3f9f015..8abb12ff92 100644 --- a/e2e/clients/dashboard-aws-multipart/app.js +++ b/e2e/clients/dashboard-aws-multipart/app.js @@ -9,7 +9,7 @@ const uppy = new Uppy() .use(Dashboard, { target: '#app', inline: true }) .use(AwsS3Multipart, { limit: 2, - companionUrl: process.env.VITE_COMPANION_URL, + endpoint: process.env.VITE_COMPANION_URL, shouldUseMultipart: true, }) diff --git a/e2e/clients/dashboard-aws/app.js b/e2e/clients/dashboard-aws/app.js index eeaa7effe6..2c23b9205b 100644 --- a/e2e/clients/dashboard-aws/app.js +++ b/e2e/clients/dashboard-aws/app.js @@ -9,7 +9,7 @@ const uppy = new Uppy() .use(Dashboard, { target: '#app', inline: true }) .use(AwsS3, { limit: 2, - companionUrl: process.env.VITE_COMPANION_URL, + endpoint: process.env.VITE_COMPANION_URL, shouldUseMultipart: false, }) diff --git a/examples/angular-example/src/app/app.component.ts b/examples/angular-example/src/app/app.component.ts index e43cf97e98..5daa5ffb47 100644 --- a/examples/angular-example/src/app/app.component.ts +++ b/examples/angular-example/src/app/app.component.ts @@ -65,8 +65,7 @@ export class AppComponent implements OnInit { }, } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - uppy: Uppy = new Uppy({ debug: true, autoProceed: true }) + uppy = new Uppy({ debug: true, autoProceed: true }) ngOnInit(): void { this.uppy diff --git a/examples/aws-companion/server.cjs b/examples/aws-companion/server.cjs index 98dc312312..c64f9830f0 100644 --- a/examples/aws-companion/server.cjs +++ b/examples/aws-companion/server.cjs @@ -29,7 +29,7 @@ const options = { }, }, s3: { - getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`, + getKey: ({ filename }) => `${crypto.randomUUID()}-${filename}`, key: process.env.COMPANION_AWS_KEY, secret: process.env.COMPANION_AWS_SECRET, bucket: process.env.COMPANION_AWS_BUCKET, diff --git a/examples/aws-nodejs/README.md b/examples/aws-nodejs/README.md index 6bc2f94b80..1fc8c7fb54 100644 --- a/examples/aws-nodejs/README.md +++ b/examples/aws-nodejs/README.md @@ -8,42 +8,67 @@ Express.js). It uses presigned URL at the backend level. It's assumed that you are familiar with AWS, at least, with the storage service (S3) and users & policies (IAM). -These instructions are **not fit for production** but tightening the security is +These instructions are **not fit for production**, tightening the security is out of the scope here. ### S3 Setup -- Create new S3 bucket in AWS (e.g. `aws-nodejs`). -- Add a bucket policy. - - ```json - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "PublicAccess", - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::aws-nodejs/*" - } - ] - } - ``` - -- Make the S3 bucket public. -- Add CORS configuration. - - ```json - [ - { - "AllowedHeaders": ["*"], - "AllowedMethods": ["GET", "PUT", "HEAD", "POST", "DELETE"], - "AllowedOrigins": ["*"], - "ExposeHeaders": [] - } - ] - ``` +Assuming you’re trying to setup the user `MY-UPPY-USER` to put the uploaded +files to the bucket `MY-UPPY-BUCKET`, here’s how you can allow `MY-UPPY-USER` to +get STS Federated Token and upload files to `MY-UPPY-BUCKET`: + +1. Set CORS settings on `MY-UPPY-BUCKET` bucket: + + ```json + [ + { + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET", "PUT", "HEAD", "POST", "DELETE"], + "AllowedOrigins": ["*"], + "ExposeHeaders": ["ETag", "Location"] + } + ] + ``` + +2. Add the following Policy to `MY-UPPY-BUCKET`: + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "MyMultipartPolicyStatement1", + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::*:user/MY-UPPY-USER" + }, + "Action": [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:ListMultipartUploadParts", + "s3:AbortMultipartUpload" + ], + "Resource": "arn:aws:s3:::MY-UPPY-BUCKET/*" + } + ] + } + ``` + +3. Add the following Policy to `MY-UPPY-USER`: (if you don’t want to enable + signing on the client, you can skip this step) + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "MyStsPolicyStatement1", + "Effect": "Allow", + "Action": ["sts:GetFederationToken"], + "Resource": ["arn:aws:sts::*:federated-user/*"] + } + ] + } + ``` ### AWS Credentials @@ -55,21 +80,6 @@ You may use existing AWS credentials or create a new user in the IAM page. [environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html) or a [credentials file in `~/.aws/credentials`](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html). -- You will need at least `PutObject` and `PutObjectAcl` permissions. - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": ["s3:PutObject", "s3:PutObjectAcl"], - "Resource": "arn:aws:s3:::aws-nodejs/*" - } - ] -} -``` ## Prerequisites @@ -83,7 +93,7 @@ Add a `.env` file to the root directory and define the S3 bucket name and port variables like the example below: ``` -COMPANION_AWS_BUCKET=aws-nodejs +COMPANION_AWS_BUCKET=MY-UPPY-BUCKET COMPANION_AWS_REGION=… COMPANION_AWS_KEY=… COMPANION_AWS_SECRET=… @@ -104,6 +114,4 @@ corepack yarn workspace @uppy-example/aws-nodejs start Dashboard demo should now be available at http://localhost:8080. -You have also a Drag & Drop demo on http://localhost:8080/drag. - _Feel free to check how the demo works and feel free to open an issue._ diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index 2f13cc770d..20f8a003b4 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -2,6 +2,7 @@ const path = require('node:path') const crypto = require('node:crypto') +const { existsSync } = require('node:fs') require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') }) const express = require('express') @@ -9,6 +10,7 @@ const express = require('express') const app = express() const port = process.env.PORT ?? 8080 +const accessControlAllowOrigin = '*' // You should define the actual domain(s) that are allowed to make requests. const bodyParser = require('body-parser') const { @@ -21,19 +23,14 @@ const { UploadPartCommand, } = require('@aws-sdk/client-s3') const { getSignedUrl } = require('@aws-sdk/s3-request-presigner') -const { - STSClient, - GetFederationTokenCommand, -} = require('@aws-sdk/client-sts') +const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts') const policy = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', - Action: [ - 's3:PutObject', - ], + Action: ['s3:PutObject'], Resource: [ `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`, `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`, @@ -54,10 +51,10 @@ let stsClient const expiresIn = 900 // Define how long until a S3 signature expires. -function getS3Client () { +function getS3Client() { s3Client ??= new S3Client({ region: process.env.COMPANION_AWS_REGION, - credentials : { + credentials: { accessKeyId: process.env.COMPANION_AWS_KEY, secretAccessKey: process.env.COMPANION_AWS_SECRET, }, @@ -65,10 +62,10 @@ function getS3Client () { return s3Client } -function getSTSClient () { +function getSTSClient() { stsClient ??= new STSClient({ region: process.env.COMPANION_AWS_REGION, - credentials : { + credentials: { accessKeyId: process.env.COMPANION_AWS_KEY, secretAccessKey: process.env.COMPANION_AWS_SECRET, }, @@ -78,53 +75,61 @@ function getSTSClient () { app.use(bodyParser.urlencoded({ extended: true }), bodyParser.json()) -app.get('/', (req, res) => { - const htmlPath = path.join(__dirname, 'public', 'index.html') - res.sendFile(htmlPath) -}) - -app.get('/drag', (req, res) => { - const htmlPath = path.join(__dirname, 'public', 'drag.html') - res.sendFile(htmlPath) +app.get('/s3/sts', (req, res, next) => { + // Before giving the STS token to the client, you should first check is they + // are authorized to perform that operation, and if the request is legit. + // For the sake of simplification, we skip that check in this example. + + getSTSClient() + .send( + new GetFederationTokenCommand({ + Name: '123user', + // The duration, in seconds, of the role session. The value specified + // can range from 900 seconds (15 minutes) up to the maximum session + // duration set for the role. + DurationSeconds: expiresIn, + Policy: JSON.stringify(policy), + }), + ) + .then((response) => { + // Test creating multipart upload from the server — it works + // createMultipartUploadYo(response) + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) + res.setHeader('Cache-Control', `public,max-age=${expiresIn}`) + res.json({ + credentials: response.Credentials, + bucket: process.env.COMPANION_AWS_BUCKET, + region: process.env.COMPANION_AWS_REGION, + }) + }, next) }) +const signOnServer = (req, res, next) => { + // Before giving the signature to the user, you should first check is they + // are authorized to perform that operation, and if the request is legit. + // For the sake of simplification, we skip that check in this example. -app.get('/sts', (req, res, next) => { - getSTSClient().send(new GetFederationTokenCommand({ - Name: '123user', - // The duration, in seconds, of the role session. The value specified - // can range from 900 seconds (15 minutes) up to the maximum session - // duration set for the role. - DurationSeconds: expiresIn, - Policy: JSON.stringify(policy), - })).then(response => { - // Test creating multipart upload from the server — it works - // createMultipartUploadYo(response) - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Cache-Control', `public,max-age=${expiresIn}`) - res.json({ - credentials: response.Credentials, - bucket: process.env.COMPANION_AWS_BUCKET, - region: process.env.COMPANION_AWS_REGION, - }) - }, next) -}) -app.post('/sign-s3', (req, res, next) => { const Key = `${crypto.randomUUID()}-${req.body.filename}` const { contentType } = req.body - getSignedUrl(getS3Client(), new PutObjectCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key, - ContentType: contentType, - }), { expiresIn }).then((url) => { - res.setHeader('Access-Control-Allow-Origin', '*') + getSignedUrl( + getS3Client(), + new PutObjectCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key, + ContentType: contentType, + }), + { expiresIn }, + ).then((url) => { + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ url, method: 'PUT', }) res.end() }, next) -}) +} +app.get('/s3/params', signOnServer) +app.post('/s3/sign', signOnServer) // === === // You can remove those endpoints if you only want to support the non-multipart uploads. @@ -133,7 +138,9 @@ app.post('/s3/multipart', (req, res, next) => { const client = getS3Client() const { type, metadata, filename } = req.body if (typeof filename !== 'string') { - return res.status(400).json({ error: 's3: content filename must be a string' }) + return res + .status(400) + .json({ error: 's3: content filename must be a string' }) } if (typeof type !== 'string') { return res.status(400).json({ error: 's3: content type must be a string' }) @@ -154,7 +161,7 @@ app.post('/s3/multipart', (req, res, next) => { next(err) return } - res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ key: data.Key, uploadId: data.UploadId, @@ -162,7 +169,7 @@ app.post('/s3/multipart', (req, res, next) => { }) }) -function validatePartNumber (partNumber) { +function validatePartNumber(partNumber) { // eslint-disable-next-line no-param-reassign partNumber = Number(partNumber) return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000 @@ -172,20 +179,33 @@ app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { const { key } = req.query if (!validatePartNumber(partNumber)) { - return res.status(400).json({ error: 's3: the part number must be an integer between 1 and 10000.' }) + return res + .status(400) + .json({ + error: 's3: the part number must be an integer between 1 and 10000.', + }) } if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } - return getSignedUrl(getS3Client(), new UploadPartCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumber: partNumber, - Body: '', - }), { expiresIn }).then((url) => { - res.setHeader('Access-Control-Allow-Origin', '*') + return getSignedUrl( + getS3Client(), + new UploadPartCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumber: partNumber, + Body: '', + }), + { expiresIn }, + ).then((url) => { + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ url, expires: expiresIn }) }, next) }) @@ -196,39 +216,52 @@ app.get('/s3/multipart/:uploadId', (req, res, next) => { const { key } = req.query if (typeof key !== 'string') { - res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) return } const parts = [] - function listPartsPage (startAt) { - client.send(new ListPartsCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumberMarker: startAt, - }), (err, data) => { - if (err) { - next(err) - return - } - - parts.push(...data.Parts) - - if (data.IsTruncated) { - // Get the next page. - listPartsPage(data.NextPartNumberMarker) - } else { - res.json(parts) - } - }) + function listPartsPage(startAt) { + client.send( + new ListPartsCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumberMarker: startAt, + }), + (err, data) => { + if (err) { + next(err) + return + } + + parts.push(...data.Parts) + + if (data.IsTruncated) { + // Get the next page. + listPartsPage(data.NextPartNumberMarker) + } else { + res.json(parts) + } + }, + ) } listPartsPage(0) }) -function isValidPart (part) { - return part && typeof part === 'object' && Number(part.PartNumber) && typeof part.ETag === 'string' +function isValidPart(part) { + return ( + part && + typeof part === 'object' && + Number(part.PartNumber) && + typeof part.ETag === 'string' + ) } app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const client = getS3Client() @@ -237,29 +270,41 @@ app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const { parts } = req.body if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } if (!Array.isArray(parts) || !parts.every(isValidPart)) { - return res.status(400).json({ error: 's3: `parts` must be an array of {ETag, PartNumber} objects.' }) + return res + .status(400) + .json({ + error: 's3: `parts` must be an array of {ETag, PartNumber} objects.', + }) } - return client.send(new CompleteMultipartUploadCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - MultipartUpload: { - Parts: parts, + return client.send( + new CompleteMultipartUploadCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + MultipartUpload: { + Parts: parts, + }, + }), + (err, data) => { + if (err) { + next(err) + return + } + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) + res.json({ + location: data.Location, + }) }, - }), (err, data) => { - if (err) { - next(err) - return - } - res.setHeader('Access-Control-Allow-Origin', '*') - res.json({ - location: data.Location, - }) - }) + ) }) app.delete('/s3/multipart/:uploadId', (req, res, next) => { @@ -268,24 +313,89 @@ app.delete('/s3/multipart/:uploadId', (req, res, next) => { const { key } = req.query if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } - return client.send(new AbortMultipartUploadCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - }), (err) => { - if (err) { - next(err) - return - } - res.json({}) - }) + return client.send( + new AbortMultipartUploadCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + }), + (err) => { + if (err) { + next(err) + return + } + res.json({}) + }, + ) }) // === === +// === === + +app.get('/', (req, res) => { + res.setHeader('Content-Type', 'text/html') + const htmlPath = path.join(__dirname, 'public', 'index.html') + res.sendFile(htmlPath) +}) +app.get('/index.html', (req, res) => { + res.setHeader('Location', '/').sendStatus(308).end() +}) +app.get('/withCustomEndpoints.html', (req, res) => { + res.setHeader('Content-Type', 'text/html') + const htmlPath = path.join(__dirname, 'public', 'withCustomEndpoints.html') + res.sendFile(htmlPath) +}) + +app.get('/uppy.min.mjs', (req, res) => { + res.setHeader('Content-Type', 'text/javascript') + const bundlePath = path.join( + __dirname, + '../..', + 'packages/uppy/dist', + 'uppy.min.mjs', + ) + if (existsSync(bundlePath)) { + res.sendFile(bundlePath) + } else { + console.warn( + 'No local JS bundle found, using the CDN as a fallback. Run `corepack yarn build` to make this warning disappear.', + ) + res.end( + 'export * from "https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs";\n', + ) + } +}) +app.get('/uppy.min.css', (req, res) => { + res.setHeader('Content-Type', 'text/css') + const bundlePath = path.join( + __dirname, + '../..', + 'packages/uppy/dist', + 'uppy.min.css', + ) + if (existsSync(bundlePath)) { + res.sendFile(bundlePath) + } else { + console.warn( + 'No local CSS bundle found, using the CDN as a fallback. Run `corepack yarn build` to make this warning disappear.', + ) + res.end( + '@import "https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.css";\n', + ) + } +}) + app.listen(port, () => { - console.log(`Example app listening on port ${port}`) + console.log(`Example app listening on port ${port}.`) + console.log(`Visit http://localhost:${port}/ on your browser to try it.`) }) +// === === diff --git a/examples/aws-nodejs/public/drag.html b/examples/aws-nodejs/public/drag.html deleted file mode 100644 index 6fbcf8e034..0000000000 --- a/examples/aws-nodejs/public/drag.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - Uppy - - - -
-
-
-
-
Uploaded files:
-
    -
    - -
    - - diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index c3a1626d86..881a07567a 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -3,264 +3,68 @@ Uppy – AWS upload example - +

    AWS upload example

    -
    +
    + Sign on the server +
    +
    +
    + Sign on the client (if WebCrypto is available) +
    +
    +
    + You seeing the simplified example, with a backend that mimicks a + Companion-like instance. See + the custom endpoint example if + you need to see how to use one or more custom function for handling + communication with the backend. +
    + diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html new file mode 100644 index 0000000000..2265a7bb70 --- /dev/null +++ b/examples/aws-nodejs/public/withCustomEndpoints.html @@ -0,0 +1,267 @@ + + + + + Uppy – AWS upload example + + + +

    AWS upload example

    +
    +
    + You seeing the complex example, with a backend that does not mimick a + Companion-like instance. See + the simplified example if you don't need custom + functions for handling communication with the backend. +
    + + + + diff --git a/examples/cdn-example/index.html b/examples/cdn-example/index.html index 7be8348a21..2b7fcd075d 100644 --- a/examples/cdn-example/index.html +++ b/examples/cdn-example/index.html @@ -5,7 +5,7 @@ @@ -19,7 +19,7 @@ Dashboard, Webcam, Tus, - } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.10/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs' const uppy = new Uppy({ debug: true, autoProceed: false }) .use(Dashboard, { trigger: '#uppyModalOpener' }) diff --git a/examples/digitalocean-spaces/server.cjs b/examples/digitalocean-spaces/server.cjs index dc369fa8b5..f98219648f 100644 --- a/examples/digitalocean-spaces/server.cjs +++ b/examples/digitalocean-spaces/server.cjs @@ -37,7 +37,7 @@ const { app: companionApp } = companion.app({ s3: { // This is the crucial part; set an endpoint template for the service you want to use. endpoint: 'https://{region}.digitaloceanspaces.com', - getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`, + getKey: ({ filename }) => `${crypto.randomUUID()}-${filename}`, key: process.env.COMPANION_AWS_KEY, secret: process.env.COMPANION_AWS_SECRET, diff --git a/examples/uppy-with-companion/client/index.html b/examples/uppy-with-companion/client/index.html index 53b6e10376..9220afbca0 100644 --- a/examples/uppy-with-companion/client/index.html +++ b/examples/uppy-with-companion/client/index.html @@ -5,7 +5,7 @@ @@ -19,7 +19,7 @@ Instagram, GoogleDrive, Tus, - } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.10/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs' const uppy = new Uppy({ debug: true, autoProceed: false }) .use(Dashboard, { trigger: '#uppyModalOpener' }) diff --git a/packages/@uppy/aws-s3/package.json b/packages/@uppy/aws-s3/package.json index 79e5ababb0..7bcbbe5c9b 100644 --- a/packages/@uppy/aws-s3/package.json +++ b/packages/@uppy/aws-s3/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/aws-s3", "description": "Upload to Amazon S3 with Uppy", - "version": "4.0.0-beta.5", + "version": "4.0.0-beta.6", "license": "MIT", "main": "lib/index.js", "type": "module", diff --git a/packages/@uppy/aws-s3/src/index.test.ts b/packages/@uppy/aws-s3/src/index.test.ts index 90c0ae8933..cce88b432f 100644 --- a/packages/@uppy/aws-s3/src/index.test.ts +++ b/packages/@uppy/aws-s3/src/index.test.ts @@ -29,7 +29,7 @@ describe('AwsS3Multipart', () => { core.use(AwsS3Multipart) const awsS3Multipart = core.getPlugin('AwsS3Multipart')! - const err = 'Expected a `companionUrl` option' + const err = 'Expected a `endpoint` option' const file = {} const opts = {} @@ -330,8 +330,8 @@ describe('AwsS3Multipart', () => { beforeEach(() => { core = new Core() core.use(AwsS3Multipart, { - companionUrl: '', - companionHeaders: { + endpoint: '', + headers: { authorization: oldToken, }, }) @@ -340,7 +340,8 @@ describe('AwsS3Multipart', () => { it('companionHeader is updated before uploading file', async () => { awsS3Multipart.setOptions({ - companionHeaders: { + endpoint: 'http://localhost', + headers: { authorization: newToken, }, }) @@ -371,7 +372,8 @@ describe('AwsS3Multipart', () => { Body > awsS3Multipart.setOptions({ - companionHeaders: { + endpoint: 'http://localhost', + headers: { authorization: newToken, }, }) diff --git a/packages/@uppy/aws-s3/src/index.ts b/packages/@uppy/aws-s3/src/index.ts index e642ed1f8c..3ae8c8bc40 100644 --- a/packages/@uppy/aws-s3/src/index.ts +++ b/packages/@uppy/aws-s3/src/index.ts @@ -143,9 +143,15 @@ export interface AwsS3Part { } type AWSS3WithCompanion = { - companionUrl: string - companionHeaders?: Record - companionCookiesRule?: string + endpoint: ConstructorParameters< + typeof RequestClient + >[1]['companionUrl'] + headers?: ConstructorParameters< + typeof RequestClient + >[1]['companionHeaders'] + cookiesRule?: ConstructorParameters< + typeof RequestClient + >[1]['companionCookiesRule'] getTemporarySecurityCredentials?: true } type AWSS3WithoutCompanion = { @@ -253,11 +259,7 @@ type AWSS3MaybeMultipartWithoutCompanion< shouldUseMultipart: (file: UppyFile) => boolean } -type RequestClientOptions = Partial< - ConstructorParameters>[1] -> - -interface _AwsS3MultipartOptions extends PluginOpts, RequestClientOptions { +interface _AwsS3MultipartOptions extends PluginOpts { allowedMetaFields?: string[] | boolean limit?: number retryDelays?: number[] | null @@ -285,7 +287,6 @@ const defaultOptions = { // eslint-disable-next-line no-bitwise (file.size! >> 10) >> 10 > 100) as any as true, retryDelays: [0, 1000, 3000, 5000], - companionHeaders: {}, } satisfies Partial> export default class AwsS3Multipart< @@ -303,6 +304,7 @@ export default class AwsS3Multipart< | 'completeMultipartUpload' > & Required> & + Partial & AWSS3MultipartWithoutCompanionMandatorySignPart & AWSS3NonMultipartWithoutCompanionMandatory, M, @@ -335,8 +337,7 @@ export default class AwsS3Multipart< // We need the `as any` here because of the dynamic default options. this.type = 'uploader' this.id = this.opts.id || 'AwsS3Multipart' - // TODO: only initiate `RequestClient` is `companionUrl` is defined. - this.#client = new RequestClient(uppy, (opts as any) ?? {}) + this.#setClient(opts) const dynamicDefaultOptions = { createMultipartUpload: this.createMultipartUpload, @@ -385,10 +386,59 @@ export default class AwsS3Multipart< return this.#client } + #setClient(opts?: Partial>) { + if ( + opts == null || + !( + 'endpoint' in opts || + 'companionUrl' in opts || + 'headers' in opts || + 'companionHeaders' in opts || + 'cookiesRule' in opts || + 'companionCookiesRule' in opts + ) + ) + return + if ('companionUrl' in opts && !('endpoint' in opts)) { + this.uppy.log( + '`companionUrl` option has been removed in @uppy/aws-s3, use `endpoint` instead.', + 'warning', + ) + } + if ('companionHeaders' in opts && !('headers' in opts)) { + this.uppy.log( + '`companionHeaders` option has been removed in @uppy/aws-s3, use `headers` instead.', + 'warning', + ) + } + if ('companionCookiesRule' in opts && !('cookiesRule' in opts)) { + this.uppy.log( + '`companionCookiesRule` option has been removed in @uppy/aws-s3, use `cookiesRule` instead.', + 'warning', + ) + } + if ('endpoint' in opts) { + this.#client = new RequestClient(this.uppy, { + pluginId: this.id, + provider: 'AWS', + companionUrl: this.opts.endpoint!, + companionHeaders: this.opts.headers, + companionCookiesRule: this.opts.cookiesRule, + }) + } else { + if ('headers' in opts) { + this.#setCompanionHeaders() + } + if ('cookiesRule' in opts) { + this.#client.opts.companionCookiesRule = opts.cookiesRule + } + } + } + setOptions(newOptions: Partial>): void { this.#companionCommunicationQueue.setOptions(newOptions) - super.setOptions(newOptions) - this.#setCompanionHeaders() + super.setOptions(newOptions as any) + this.#setClient(newOptions) } /** @@ -410,9 +460,9 @@ export default class AwsS3Multipart< } #assertHost(method: string): void { - if (!this.opts.companionUrl) { + if (!this.#client) { throw new Error( - `Expected a \`companionUrl\` option containing a Companion address, or if you are not using Companion, a custom \`${method}\` implementation.`, + `Expected a \`endpoint\` option containing a URL, or if you are not using Companion, a custom \`${method}\` implementation.`, ) } } @@ -486,15 +536,18 @@ export default class AwsS3Multipart< throwIfAborted(options?.signal) if (this.#cachedTemporaryCredentials == null) { + const { getTemporarySecurityCredentials } = this.opts // We do not await it just yet, so concurrent calls do not try to override it: - if (this.opts.getTemporarySecurityCredentials === true) { + if (getTemporarySecurityCredentials === true) { this.#assertHost('getTemporarySecurityCredentials') this.#cachedTemporaryCredentials = this.#client .get('s3/sts', options) .then(assertServerError) } else { this.#cachedTemporaryCredentials = - this.opts.getTemporarySecurityCredentials(options) + (getTemporarySecurityCredentials as AWSS3WithoutCompanion['getTemporarySecurityCredentials'])!( + options, + ) } this.#cachedTemporaryCredentials = await this.#cachedTemporaryCredentials setTimeout( @@ -572,7 +625,6 @@ export default class AwsS3Multipart< abortMultipartUpload( file: UppyFile, { key, uploadId, signal }: UploadResultWithSignal, - // eslint-disable-next-line @typescript-eslint/no-unused-vars ): Promise { this.#assertHost('abortMultipartUpload') @@ -920,7 +972,7 @@ export default class AwsS3Multipart< } #setCompanionHeaders = () => { - this.#client.setCompanionHeaders(this.opts.companionHeaders) + this.#client?.setCompanionHeaders(this.opts.headers!) } #setResumableUploadsCapability = (boolean: boolean) => { diff --git a/packages/@uppy/companion/src/server/Uploader.js b/packages/@uppy/companion/src/server/Uploader.js index e3cfb3adb0..25febdb309 100644 --- a/packages/@uppy/companion/src/server/Uploader.js +++ b/packages/@uppy/companion/src/server/Uploader.js @@ -220,7 +220,7 @@ class Uploader { if (this.readStream) this.readStream.destroy(err) } - async _uploadByProtocol() { + async _uploadByProtocol(req) { // todo a default protocol should not be set. We should ensure that the user specifies their protocol. // after we drop old versions of uppy client we can remove this const protocol = this.options.protocol || PROTOCOLS.multipart @@ -229,7 +229,7 @@ class Uploader { case PROTOCOLS.multipart: return this.#uploadMultipart(this.readStream) case PROTOCOLS.s3Multipart: - return this.#uploadS3Multipart(this.readStream) + return this.#uploadS3Multipart(this.readStream, req) case PROTOCOLS.tus: return this.#uploadTus(this.readStream) default: @@ -271,8 +271,9 @@ class Uploader { /** * * @param {import('stream').Readable} stream + * @param {import('express').Request} req */ - async uploadStream(stream) { + async uploadStream(stream, req) { try { if (this.#uploadState !== states.idle) throw new Error('Can only start an upload in the idle state') if (this.readStream) throw new Error('Already uploading') @@ -290,7 +291,7 @@ class Uploader { if (this.#uploadState !== states.uploading) return undefined const { url, extraData } = await Promise.race([ - this._uploadByProtocol(), + this._uploadByProtocol(req), // If we don't handle stream errors, we get unhandled error in node. new Promise((resolve, reject) => this.readStream.on('error', reject)), ]) @@ -310,12 +311,13 @@ class Uploader { /** * * @param {import('stream').Readable} stream + * @param {import('express').Request} req */ - async tryUploadStream(stream) { + async tryUploadStream(stream, req) { try { emitter().emit('upload-start', { token: this.token }) - const ret = await this.uploadStream(stream) + const ret = await this.uploadStream(stream, req) if (!ret) return const { url, extraData } = ret this.#emitSuccess(url, extraData) @@ -637,7 +639,7 @@ class Uploader { /** * Upload the file to S3 using a Multipart upload. */ - async #uploadS3Multipart(stream) { + async #uploadS3Multipart(stream, req) { if (!this.options.s3) { throw new Error('The S3 client is not configured on this companion instance.') } @@ -647,13 +649,14 @@ class Uploader { * @type {{client: import('@aws-sdk/client-s3').S3Client, options: Record}} */ const s3Options = this.options.s3 + const { metadata } = this.options const { client, options } = s3Options const params = { - Bucket: getBucket(options.bucket, null, this.options.metadata), - Key: options.getKey(null, filename, this.options.metadata), - ContentType: this.options.metadata.type, - Metadata: rfc2047EncodeMetadata(this.options.metadata), + Bucket: getBucket({ bucketOrFn: options.bucket, req, metadata }), + Key: options.getKey({ req, filename, metadata }), + ContentType: metadata.type, + Metadata: rfc2047EncodeMetadata(metadata), Body: stream, } diff --git a/packages/@uppy/companion/src/server/controllers/s3.js b/packages/@uppy/companion/src/server/controllers/s3.js index fc189d3b27..a084de2cbe 100644 --- a/packages/@uppy/companion/src/server/controllers/s3.js +++ b/packages/@uppy/companion/src/server/controllers/s3.js @@ -52,10 +52,11 @@ module.exports = function s3 (config) { const client = getS3Client(req, res) if (!client) return - const bucket = getBucket(config.bucket, req) + const { metadata = {}, filename } = req.query - const metadata = req.query.metadata || {} - const key = config.getKey(req, req.query.filename, metadata) + const bucket = getBucket({ bucketOrFn: config.bucket, req, filename, metadata }) + + const key = config.getKey({ req, filename, metadata }) if (typeof key !== 'string') { res.status(500).json({ error: 'S3 uploads are misconfigured: filename returned from `getKey` must be a string' }) return @@ -106,8 +107,12 @@ module.exports = function s3 (config) { const client = getS3Client(req, res) if (!client) return - const key = config.getKey(req, req.body.filename, req.body.metadata || {}) - const { type, metadata } = req.body + const { type, metadata = {}, filename } = req.body + + const key = config.getKey({ req, filename, metadata }) + + const bucket = getBucket({ bucketOrFn: config.bucket, req, filename, metadata }) + if (typeof key !== 'string') { res.status(500).json({ error: 's3: filename returned from `getKey` must be a string' }) return @@ -116,7 +121,6 @@ module.exports = function s3 (config) { res.status(400).json({ error: 's3: content type must be a string' }) return } - const bucket = getBucket(config.bucket, req) const params = { Bucket: bucket, @@ -160,7 +164,7 @@ module.exports = function s3 (config) { return } - const bucket = getBucket(config.bucket, req) + const bucket = getBucket({ bucketOrFn: config.bucket, req }) const parts = [] @@ -211,7 +215,7 @@ module.exports = function s3 (config) { return } - const bucket = getBucket(config.bucket, req) + const bucket = getBucket({ bucketOrFn: config.bucket, req }) getSignedUrl(client, new UploadPartCommand({ Bucket: bucket, @@ -260,7 +264,7 @@ module.exports = function s3 (config) { return } - const bucket = getBucket(config.bucket, req) + const bucket = getBucket({ bucketOrFn: config.bucket, req }) Promise.all( partNumbersArray.map((partNumber) => { @@ -303,7 +307,7 @@ module.exports = function s3 (config) { return } - const bucket = getBucket(config.bucket, req) + const bucket = getBucket({ bucketOrFn: config.bucket, req }) client.send(new AbortMultipartUploadCommand({ Bucket: bucket, @@ -344,7 +348,7 @@ module.exports = function s3 (config) { return } - const bucket = getBucket(config.bucket, req) + const bucket = getBucket({ bucketOrFn: config.bucket, req }) client.send(new CompleteMultipartUploadCommand({ Bucket: bucket, diff --git a/packages/@uppy/companion/src/server/helpers/upload.js b/packages/@uppy/companion/src/server/helpers/upload.js index 5f637c43bf..24de627055 100644 --- a/packages/@uppy/companion/src/server/helpers/upload.js +++ b/packages/@uppy/companion/src/server/helpers/upload.js @@ -21,7 +21,7 @@ async function startDownUpload({ req, res, getSize, download }) { await uploader.awaitReady(clientSocketConnectTimeout) logger.debug('Socket connection received. Starting remote download/upload.', null, req.id) - await uploader.tryUploadStream(stream) + await uploader.tryUploadStream(stream, req) })().catch((err) => logger.error(err)) // Respond the request diff --git a/packages/@uppy/companion/src/server/helpers/utils.js b/packages/@uppy/companion/src/server/helpers/utils.js index 627d87fcb1..0f8ededaae 100644 --- a/packages/@uppy/companion/src/server/helpers/utils.js +++ b/packages/@uppy/companion/src/server/helpers/utils.js @@ -146,7 +146,7 @@ module.exports.decrypt = (encrypted, secret) => { return decrypted } -module.exports.defaultGetKey = (req, filename) => `${crypto.randomUUID()}-${filename}` +module.exports.defaultGetKey = ({ filename }) => `${crypto.randomUUID()}-${filename}` class StreamHttpJsonError extends Error { statusCode @@ -207,8 +207,22 @@ module.exports.rfc2047EncodeMetadata = (metadata) => ( Object.fromEntries(Object.entries(metadata).map((entry) => entry.map(rfc2047Encode))) ) -module.exports.getBucket = (bucketOrFn, req, metadata) => { - const bucket = typeof bucketOrFn === 'function' ? bucketOrFn(req, metadata) : bucketOrFn +/** + * + * @param {{ + * bucketOrFn: string | ((a: { + * req: import('express').Request, + * metadata: Record, + * filename: string | undefined, + * }) => string), + * req: import('express').Request, + * metadata?: Record, + * filename?: string, + * }} param0 + * @returns + */ +module.exports.getBucket = ({ bucketOrFn, req, metadata, filename }) => { + const bucket = typeof bucketOrFn === 'function' ? bucketOrFn({ req, metadata, filename }) : bucketOrFn if (typeof bucket !== 'string' || bucket === '') { // This means a misconfiguration or bug diff --git a/packages/@uppy/companion/src/standalone/helper.js b/packages/@uppy/companion/src/standalone/helper.js index 0b0d35902b..5f352e82d6 100644 --- a/packages/@uppy/companion/src/standalone/helper.js +++ b/packages/@uppy/companion/src/standalone/helper.js @@ -28,8 +28,8 @@ const getSecret = (baseEnvVar) => { * * @returns {string} */ -exports.generateSecret = () => { - logger.warn('auto-generating server secret because none was specified', 'startup.secret') +exports.generateSecret = (secretName) => { + logger.warn(`auto-generating server ${secretName} because none was specified`, 'startup.secret') return crypto.randomBytes(64).toString('hex') } diff --git a/packages/@uppy/companion/src/standalone/index.js b/packages/@uppy/companion/src/standalone/index.js index 155b081052..fefaf64f8e 100644 --- a/packages/@uppy/companion/src/standalone/index.js +++ b/packages/@uppy/companion/src/standalone/index.js @@ -22,8 +22,8 @@ module.exports = function server(inputCompanionOptions) { companion.setLoggerProcessName(companionOptions) - if (!companionOptions.secret) companionOptions.secret = generateSecret() - if (!companionOptions.preAuthSecret) companionOptions.preAuthSecret = generateSecret() + if (!companionOptions.secret) companionOptions.secret = generateSecret('secret') + if (!companionOptions.preAuthSecret) companionOptions.preAuthSecret = generateSecret('preAuthSecret') const app = express() diff --git a/packages/@uppy/companion/test/__tests__/uploader.js b/packages/@uppy/companion/test/__tests__/uploader.js index ac258c8678..3e6d1398a8 100644 --- a/packages/@uppy/companion/test/__tests__/uploader.js +++ b/packages/@uppy/companion/test/__tests__/uploader.js @@ -22,6 +22,8 @@ process.env.COMPANION_DATADIR = './test/output' process.env.COMPANION_DOMAIN = 'localhost:3020' const { companionOptions } = standalone() +const mockReq = {} + describe('uploader with tus protocol', () => { test('uploader respects uploadUrls', async () => { const opts = { @@ -87,7 +89,7 @@ describe('uploader with tus protocol', () => { }) socketClient.onUploadSuccess(uploadToken, onUploadSuccess) await promise - await uploader.tryUploadStream(stream) + await uploader.tryUploadStream(stream, mockReq) expect(firstReceivedProgress).toBe(8) @@ -136,7 +138,7 @@ describe('uploader with tus protocol', () => { return new Promise((resolve, reject) => { // validate that the test is resolved on socket connection uploader.awaitReady(60000).then(() => { - uploader.tryUploadStream(stream).then(() => { + uploader.tryUploadStream(stream, mockReq).then(() => { try { expect(fs.existsSync(uploader.path)).toBe(false) resolve() @@ -286,7 +288,7 @@ describe('uploader with tus protocol', () => { const uploadToken = uploader.token // validate that the test is resolved on socket connection - uploader.awaitReady(60000).then(() => uploader.tryUploadStream(stream)) + uploader.awaitReady(60000).then(() => uploader.tryUploadStream(stream, mockReq)) socketClient.connect(uploadToken) return new Promise((resolve, reject) => { diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 66996e46a8..ae297d6abe 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -308,7 +308,7 @@ const defaultUploadState = { * Manages plugins, state updates, acts as an event bus, * adds/removes files and metadata. */ -export class Uppy { +export class Uppy> { static VERSION = packageJson.version #plugins: Record[]> = Object.create(null) @@ -1724,10 +1724,12 @@ export class Uppy { /** * Find one Plugin by name. */ - getPlugin(id: string): UnknownPlugin | undefined { + getPlugin = UnknownPlugin>( + id: string, + ): T | undefined { for (const plugins of Object.values(this.#plugins)) { const foundPlugin = plugins.find((plugin) => plugin.id === id) - if (foundPlugin != null) return foundPlugin + if (foundPlugin != null) return foundPlugin as T } return undefined } diff --git a/packages/@uppy/core/src/types.test.ts b/packages/@uppy/core/src/types.test.ts index e73cea2f4c..232c3c230e 100644 --- a/packages/@uppy/core/src/types.test.ts +++ b/packages/@uppy/core/src/types.test.ts @@ -1,7 +1,7 @@ import { expectTypeOf, test } from 'vitest' import type { Body, InternalMetadata, Meta } from '@uppy/utils/lib/UppyFile' -import Uppy from './Uppy' +import Uppy, { type UnknownPlugin } from './Uppy' import UIPlugin, { type UIPluginOptions } from './UIPlugin' interface Opts extends UIPluginOptions { @@ -17,12 +17,24 @@ class TestPlugin extends UIPlugin { test('can use Uppy class without generics', async () => { const core = new Uppy() - expectTypeOf(core).toEqualTypeOf>() + expectTypeOf(core).toEqualTypeOf>>() }) test('can .use() a plugin', async () => { const core = new Uppy().use(TestPlugin) - expectTypeOf(core).toEqualTypeOf>() + expectTypeOf(core).toEqualTypeOf>>() +}) + +test('can .getPlugin() with a generic', async () => { + const core = new Uppy().use(TestPlugin) + const plugin = core.getPlugin>('TestPlugin') + const plugin2 = core.getPlugin('TestPlugin') + expectTypeOf(plugin).toEqualTypeOf | undefined>() + expectTypeOf(plugin2).toEqualTypeOf< + // The default type + | UnknownPlugin, Record> + | undefined + >() }) test('Meta and Body generic move through the Uppy class', async () => { diff --git a/packages/@uppy/locales/package.json b/packages/@uppy/locales/package.json index e0357c7547..710a5f913c 100644 --- a/packages/@uppy/locales/package.json +++ b/packages/@uppy/locales/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/locales", "description": "Uppy language packs", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "license": "MIT", "type": "module", "keywords": [ diff --git a/packages/@uppy/locales/src/fa_IR.ts b/packages/@uppy/locales/src/fa_IR.ts index c1ce983b23..07687df926 100644 --- a/packages/@uppy/locales/src/fa_IR.ts +++ b/packages/@uppy/locales/src/fa_IR.ts @@ -223,3 +223,5 @@ fa_IR.strings = { zoomIn: 'بزرگ‌نمایی', zoomOut: 'کوچک‌نمایی', } + +export default fa_IR diff --git a/packages/@uppy/locales/src/it_IT.ts b/packages/@uppy/locales/src/it_IT.ts index 6907b9e25d..570a34ea2c 100644 --- a/packages/@uppy/locales/src/it_IT.ts +++ b/packages/@uppy/locales/src/it_IT.ts @@ -91,6 +91,7 @@ it_IT.strings = { resumeUpload: "Riprendi l'upload", retry: 'Riprova', retryUpload: "Riprova l'upload", + save: 'Salva', saveChanges: 'Salva le modifiche', selectX: { '0': 'Seleziona %{smart_count}', diff --git a/packages/@uppy/provider-views/CHANGELOG.md b/packages/@uppy/provider-views/CHANGELOG.md index e3e79ba8e8..0aa5d03260 100644 --- a/packages/@uppy/provider-views/CHANGELOG.md +++ b/packages/@uppy/provider-views/CHANGELOG.md @@ -24,6 +24,12 @@ Included in: Uppy v4.0.0-beta.1 - @uppy/provider-views: fix `super.toggleCheckbox` bug (Mikael Finstad / #5004) - @uppy/core,@uppy/provider-views: Fix breadcrumbs (Evgenia Karunus / #4986) +## 3.12.1 + +Released: 2024-06-11 +Included in: Uppy v3.26.1 + +- @uppy/provider-views: fix wrong font for files (Merlijn Vos / #5234) ## 3.12.0 diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index cec26980d8..285d84960e 100644 --- a/packages/@uppy/provider-views/package.json +++ b/packages/@uppy/provider-views/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/provider-views", "description": "View library for Uppy remote provider plugins.", - "version": "4.0.0-beta.7", + "version": "4.0.0-beta.8", "license": "MIT", "main": "lib/index.js", "style": "dist/style.min.css", diff --git a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx similarity index 90% rename from packages/@uppy/provider-views/src/Item/components/GridLi.tsx rename to packages/@uppy/provider-views/src/Item/components/GridItem.tsx index 5fc1a6514f..766e4d5508 100644 --- a/packages/@uppy/provider-views/src/Item/components/GridLi.tsx +++ b/packages/@uppy/provider-views/src/Item/components/GridItem.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import type { RestrictionError } from '@uppy/core/lib/Restricter' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' -type GridListItemProps = { +type GridItemProps = { className: string isDisabled: boolean restrictionError?: RestrictionError | null @@ -18,8 +18,8 @@ type GridListItemProps = { children?: ComponentChildren } -function GridListItem( - props: GridListItemProps, +function GridItem( + props: GridItemProps, ): h.JSX.Element { const { className, @@ -73,4 +73,4 @@ function GridListItem( ) } -export default GridListItem +export default GridItem diff --git a/packages/@uppy/provider-views/src/Item/components/ItemIcon.tsx b/packages/@uppy/provider-views/src/Item/components/ItemIcon.tsx index 0058871904..f6461b8059 100644 --- a/packages/@uppy/provider-views/src/Item/components/ItemIcon.tsx +++ b/packages/@uppy/provider-views/src/Item/components/ItemIcon.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import { h } from 'preact' function FileIcon() { @@ -44,11 +43,15 @@ function VideoIcon() { ) } -export default function ItemIcon(props: { +type ItemIconProps = { itemIconString: string alt?: string -}): h.JSX.Element | null { - const { itemIconString } = props +} + +export default function ItemIcon({ + itemIconString, + alt = undefined, +}: ItemIconProps): h.JSX.Element | null { if (itemIconString === null) return null switch (itemIconString) { @@ -59,7 +62,6 @@ export default function ItemIcon(props: { case 'video': return default: { - const { alt } = props return ( = { showTitles: boolean @@ -46,7 +46,7 @@ export default function Item( switch (viewType) { case 'grid': return ( - + // eslint-disable-next-line react/jsx-props-no-spreading {...props} className={className} @@ -64,7 +64,7 @@ export default function Item( ) case 'unsplash': return ( - + // eslint-disable-next-line react/jsx-props-no-spreading {...props} className={className} @@ -79,7 +79,7 @@ export default function Item( > {author!.name} - + ) default: throw new Error(`There is no such type ${viewType}`) diff --git a/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx b/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx index 5b07df30ce..099e46a82c 100644 --- a/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/AuthView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import { h } from 'preact' import { useCallback } from 'preact/hooks' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' @@ -105,18 +104,14 @@ const defaultRenderForm = ({ onAuth: AuthViewProps['handleAuth'] }) => -export default function AuthView( - props: AuthViewProps, -) { - const { - loading, - pluginName, - pluginIcon, - i18n, - handleAuth, - renderForm = defaultRenderForm, - } = props - +export default function AuthView({ + loading, + pluginName, + pluginIcon, + i18n, + handleAuth, + renderForm = defaultRenderForm, +}: AuthViewProps) { return (
    {pluginIcon()}
    diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss index 29d12d67a3..6ad10f7f92 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss @@ -70,6 +70,7 @@ display: flex; align-items: center; color: inherit; + font-family: $font-family-base; // For better outline padding: 2px; diff --git a/packages/@uppy/react/CHANGELOG.md b/packages/@uppy/react/CHANGELOG.md index cb1154d8e5..02082ca0bc 100644 --- a/packages/@uppy/react/CHANGELOG.md +++ b/packages/@uppy/react/CHANGELOG.md @@ -1,5 +1,12 @@ # @uppy/react +## 4.0.0-beta.6 + +Released: 2024-06-11 +Included in: Uppy v4.0.0-beta.11 + +- @uppy/react: remove `react:` prefix from `id` & allow `id` as a prop (Merlijn Vos / #5228) + ## 4.0.0-beta.4 Released: 2024-04-29 diff --git a/packages/@uppy/react/package.json b/packages/@uppy/react/package.json index bdf3d5690c..89aead9e17 100644 --- a/packages/@uppy/react/package.json +++ b/packages/@uppy/react/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/react", "description": "React component wrappers around Uppy's official UI plugins.", - "version": "4.0.0-beta.5", + "version": "4.0.0-beta.6", "license": "MIT", "main": "lib/index.js", "type": "module", diff --git a/packages/@uppy/react/src/Dashboard.ts b/packages/@uppy/react/src/Dashboard.ts index 3b32f29aa1..0ab0b0d1e3 100644 --- a/packages/@uppy/react/src/Dashboard.ts +++ b/packages/@uppy/react/src/Dashboard.ts @@ -51,9 +51,9 @@ class Dashboard extends Component< installPlugin(): void { const { uppy, ...options } = { - id: 'react:Dashboard', - inline: true, + id: 'Dashboard', ...this.props, + inline: true, target: this.container, } uppy.use(DashboardPlugin, options) diff --git a/packages/@uppy/react/src/DashboardModal.ts b/packages/@uppy/react/src/DashboardModal.ts index 39672990ce..570e87a991 100644 --- a/packages/@uppy/react/src/DashboardModal.ts +++ b/packages/@uppy/react/src/DashboardModal.ts @@ -73,8 +73,8 @@ class DashboardModal extends Component< ...rest } = this.props const options = { + id: 'DashboardModal', ...rest, - id: 'react:DashboardModal', inline: false, target, open, diff --git a/packages/@uppy/react/src/DragDrop.ts b/packages/@uppy/react/src/DragDrop.ts index 177dc6f5d7..a7a6171c69 100644 --- a/packages/@uppy/react/src/DragDrop.ts +++ b/packages/@uppy/react/src/DragDrop.ts @@ -43,9 +43,9 @@ class DragDrop extends Component< } installPlugin(): void { - const { uppy, locale, inputName, width, height, note } = this.props + const { uppy, locale, inputName, width, height, note, id } = this.props const options = { - id: 'react:DragDrop', + id: id || 'DragDrop', locale, inputName, width, diff --git a/packages/@uppy/react/src/FileInput.ts b/packages/@uppy/react/src/FileInput.ts index 385cb1eab1..ec0d3f12e5 100644 --- a/packages/@uppy/react/src/FileInput.ts +++ b/packages/@uppy/react/src/FileInput.ts @@ -1,10 +1,11 @@ import { createElement as h, Component } from 'react' -import type { UnknownPlugin, Uppy } from '@uppy/core' +import type { UIPluginOptions, UnknownPlugin, Uppy } from '@uppy/core' import FileInputPlugin from '@uppy/file-input' import type { Body, Meta } from '@uppy/utils/lib/UppyFile' import type { Locale } from '@uppy/utils/lib/Translator' -interface FileInputProps { +interface FileInputProps + extends UIPluginOptions { uppy: Uppy locale?: Locale pretty?: boolean @@ -47,9 +48,9 @@ class FileInput extends Component< } installPlugin(): void { - const { uppy, locale, pretty, inputName } = this.props + const { uppy, locale, pretty, inputName, id } = this.props const options = { - id: 'react:FileInput', + id: id || 'FileInput', locale, pretty, inputName, diff --git a/packages/@uppy/react/src/ProgressBar.ts b/packages/@uppy/react/src/ProgressBar.ts index a58fc2f147..6c94863789 100644 --- a/packages/@uppy/react/src/ProgressBar.ts +++ b/packages/@uppy/react/src/ProgressBar.ts @@ -42,9 +42,9 @@ class ProgressBar extends Component< } installPlugin(): void { - const { uppy, fixed, hideAfterFinish } = this.props + const { uppy, fixed, hideAfterFinish, id } = this.props const options = { - id: 'react:ProgressBar', + id: id || 'ProgressBar', fixed, hideAfterFinish, target: this.container, diff --git a/packages/@uppy/react/src/StatusBar.ts b/packages/@uppy/react/src/StatusBar.ts index 7644460607..fd2567ec9e 100644 --- a/packages/@uppy/react/src/StatusBar.ts +++ b/packages/@uppy/react/src/StatusBar.ts @@ -52,9 +52,10 @@ class StatusBar extends Component< showProgressDetails, hideAfterFinish, doneButtonHandler, + id, } = this.props const options = { - id: 'react:StatusBar', + id: id || 'StatusBar', hideUploadButton, hideRetryButton, hidePauseResumeButton, diff --git a/packages/@uppy/transloadit/package.json b/packages/@uppy/transloadit/package.json index fd5f70c2d2..db996e68b0 100644 --- a/packages/@uppy/transloadit/package.json +++ b/packages/@uppy/transloadit/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/transloadit", "description": "The Transloadit plugin can be used to upload files to Transloadit for all kinds of processing, such as transcoding video, resizing images, zipping/unzipping, and more", - "version": "4.0.0-beta.7", + "version": "4.0.0-beta.8", "license": "MIT", "main": "lib/index.js", "type": "module", diff --git a/packages/@uppy/transloadit/src/Assembly.ts b/packages/@uppy/transloadit/src/Assembly.ts index 765639f88d..de8ae1ff82 100644 --- a/packages/@uppy/transloadit/src/Assembly.ts +++ b/packages/@uppy/transloadit/src/Assembly.ts @@ -104,14 +104,14 @@ class TransloaditAssembly extends Emitter { this.#sse.addEventListener('assembly_upload_finished', (e) => { const file = JSON.parse(e.data) - this.emit('upload', file) this.status.uploads.push(file) + this.emit('upload', file) }) this.#sse.addEventListener('assembly_result_finished', (e) => { const [stepName, result] = JSON.parse(e.data) - this.emit('result', stepName, result) ;(this.status.results[stepName] ??= []).push(result) + this.emit('result', stepName, result) }) this.#sse.addEventListener('assembly_execution_progress', (e) => { diff --git a/packages/uppy/package.json b/packages/uppy/package.json index f83e4bba91..e5186b56f9 100644 --- a/packages/uppy/package.json +++ b/packages/uppy/package.json @@ -1,7 +1,7 @@ { "name": "uppy", "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:", - "version": "4.0.0-beta.10", + "version": "4.0.0-beta.11", "license": "MIT", "main": "index.mjs", "module": "index.mjs", diff --git a/private/remark-lint-uppy/package.json b/private/remark-lint-uppy/package.json index f608cb7af5..f5f8b47c43 100644 --- a/private/remark-lint-uppy/package.json +++ b/private/remark-lint-uppy/package.json @@ -26,7 +26,6 @@ "retext-equality": "^7.0.0", "retext-profanities": "^8.0.0", "retext-quotes": "^6.0.0", - "retext-simplify": "^8.0.0", "retext-syntax-mentions": "^4.0.0", "unified": "^11.0.0", "unified-message-control": "^4.0.0" diff --git a/private/remark-lint-uppy/retext-preset.js b/private/remark-lint-uppy/retext-preset.js index afe6d3ecf8..774e4b7add 100644 --- a/private/remark-lint-uppy/retext-preset.js +++ b/private/remark-lint-uppy/retext-preset.js @@ -1,45 +1,28 @@ import remarkRetext from 'remark-retext' import { unified } from 'unified' import retextEnglish from 'retext-english' +// eslint-disable-next-line import/no-unresolved import retextEquality from 'retext-equality' +// eslint-disable-next-line import/no-unresolved import retextProfanities from 'retext-profanities' import retextQuotes from 'retext-quotes' -import retextSimplify from 'retext-simplify' import retextSyntaxMentions from 'retext-syntax-mentions' export default [ remarkRetext, unified() .use(retextEnglish) - .use(retextEquality, { ignore: ['disabled', 'host', 'hosts', 'invalid', 'whitespace', 'of course'] }) - .use(retextProfanities, { sureness: 1 }) - .use(retextQuotes) - .use(retextSimplify, { + .use(retextEquality, { ignore: [ - 'accurate', - 'address', - 'alternatively', - 'component', - 'equivalent', - 'function', - 'identify', - 'implement', - 'initial', - 'interface', - 'maintain', - 'maximum', - 'minimum', - 'option', - 'parameters', - 'provide', - 'render', - 'request', - 'selection', - 'submit', - 'type', - 'validate', - 'however', + 'disabled', + 'host', + 'hosts', + 'invalid', + 'whitespace', + 'of course', ], }) + .use(retextProfanities, { sureness: 1 }) + .use(retextQuotes) .use(retextSyntaxMentions), ] diff --git a/yarn.lock b/yarn.lock index ba9dd5c5a1..b49a40b731 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8147,7 +8147,6 @@ __metadata: retext-equality: "npm:^7.0.0" retext-profanities: "npm:^8.0.0" retext-quotes: "npm:^6.0.0" - retext-simplify: "npm:^8.0.0" retext-syntax-mentions: "npm:^4.0.0" unified: "npm:^11.0.0" unified-message-control: "npm:^4.0.0" @@ -25927,20 +25926,6 @@ __metadata: languageName: node linkType: hard -"retext-simplify@npm:^8.0.0": - version: 8.0.0 - resolution: "retext-simplify@npm:8.0.0" - dependencies: - "@types/nlcst": "npm:^2.0.0" - nlcst-search: "npm:^4.0.0" - nlcst-to-string: "npm:^4.0.0" - quotation: "npm:^2.0.0" - unist-util-position: "npm:^5.0.0" - vfile: "npm:^6.0.0" - checksum: 10/f17f5a27abc857383ba6dc78263e970cac42100d6de408e6c883c30d906ba7c399e1a21967585b9f1edd97b101bd586530ecca17b8c42a253e6f5df2a72323e5 - languageName: node - linkType: hard - "retext-syntax-mentions@npm:^4.0.0": version: 4.0.0 resolution: "retext-syntax-mentions@npm:4.0.0"