diff --git a/BUNDLE-README.md b/BUNDLE-README.md index 19d7c854c..f537d98e9 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/README.md b/README.md index 4fc71a92e..2ab19c7c6 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus ``` Add CSS -[uppy.min.css](https://releases.transloadit.com/uppy/v5.1.12/uppy.min.css), +[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.0/uppy.min.css), either to your HTML page’s `` or include in JS, if your bundler of choice supports it. @@ -117,7 +117,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object. ```html @@ -128,7 +128,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object. Uppy, Dashboard, Tus, - } from 'https://releases.transloadit.com/uppy/v5.1.12/uppy.min.mjs' + } from 'https://releases.transloadit.com/uppy/v5.2.0/uppy.min.mjs' const uppy = new Uppy() uppy.use(Dashboard, { target: '#files-drag-drop' }) diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 6deb903be..b0058532d 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -16,7 +16,7 @@ "@uppy/transloadit": "workspace:*", "@uppy/tus": "workspace:*", "@uppy/xhr-upload": "workspace:*", - "next": "15.5.2", + "next": "15.5.7", "react": "19.1.0", "react-dom": "19.1.0" }, diff --git a/packages/@uppy/image-generator/CHANGELOG.md b/packages/@uppy/image-generator/CHANGELOG.md new file mode 100644 index 000000000..3222febdd --- /dev/null +++ b/packages/@uppy/image-generator/CHANGELOG.md @@ -0,0 +1,14 @@ +# @uppy/image-generator + +## 1.0.0 + +### Major Changes + +- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit + +### Patch Changes + +- Updated dependencies [5684efa] +- Updated dependencies [5684efa] + - @uppy/provider-views@5.2.1 + - @uppy/transloadit@5.4.0 diff --git a/packages/@uppy/image-generator/LICENSE b/packages/@uppy/image-generator/LICENSE new file mode 100644 index 000000000..d074cabcb --- /dev/null +++ b/packages/@uppy/image-generator/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/@uppy/image-generator/README.md b/packages/@uppy/image-generator/README.md new file mode 100644 index 000000000..0a35b9d95 --- /dev/null +++ b/packages/@uppy/image-generator/README.md @@ -0,0 +1,49 @@ +# @uppy/image-generator + +Uppy logo: a smiling puppy above a pink upwards arrow + +[![npm version](https://img.shields.io/npm/v/@uppy/image-generator.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/image-generator) +![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/Tests/badge.svg) +![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg) +![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg) + +**[Read the docs](https://uppy.io/docs/image-generator)** | +**[Try it](https://uppy.io/examples/)** + +Uppy is being developed by the folks at [Transloadit](https://transloadit.com), +a versatile file encoding service. + +## Example + +```js +import Uppy from '@uppy/core' +import ImageGenerator from '@uppy/image-generator' + +const uppy = new Uppy() + .use(ImageGenerator, { + assemblyOptions: async (prompt) => { + const res = await fetch(`/assembly-options?prompt=${encodeURIComponent(prompt)}`) + return res.json() + } + }) +``` + +## Installation + +```bash +$ npm install @uppy/image-generator +``` + +Alternatively, you can also use this plugin in a pre-built bundle from +Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global +`window.Uppy` object. See the +[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions. + +## Documentation + +Documentation for this plugin can be found on the +[Uppy website](https://uppy.io/docs/image-generator). + +## License + +[The MIT License](./LICENSE). diff --git a/packages/@uppy/image-generator/package.json b/packages/@uppy/image-generator/package.json new file mode 100644 index 000000000..4b555a518 --- /dev/null +++ b/packages/@uppy/image-generator/package.json @@ -0,0 +1,60 @@ +{ + "name": "@uppy/image-generator", + "description": "AI-powered image generation for Uppy", + "version": "1.0.0", + "license": "MIT", + "sideEffects": false, + "type": "module", + "keywords": [ + "file uploader", + "upload", + "uppy", + "uppy-plugin", + "ai", + "image generation" + ], + "homepage": "https://uppy.io", + "bugs": { + "url": "https://github.com/transloadit/uppy/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/transloadit/uppy.git" + }, + "files": [ + "src", + "lib", + "dist", + "CHANGELOG.md" + ], + "exports": { + ".": "./lib/index.js", + "./css/style.css": "./dist/style.css", + "./css/style.min.css": "./dist/style.min.css", + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsc --build tsconfig.build.json", + "build:css": "sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css", + "typecheck": "tsc --build" + }, + "dependencies": { + "@uppy/provider-views": "workspace:^", + "@uppy/transloadit": "workspace:^", + "@uppy/utils": "workspace:^", + "preact": "^10.5.13" + }, + "devDependencies": { + "@uppy/core": "workspace:^", + "cssnano": "^7.0.7", + "postcss": "^8.5.6", + "postcss-cli": "^11.0.1", + "sass": "^1.89.2" + }, + "peerDependencies": { + "@uppy/core": "workspace:^" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@uppy/image-generator/src/index.tsx b/packages/@uppy/image-generator/src/index.tsx new file mode 100644 index 000000000..8fc002e32 --- /dev/null +++ b/packages/@uppy/image-generator/src/index.tsx @@ -0,0 +1,381 @@ +import type { Body, Meta, MinimalRequiredUppyFile, Uppy } from '@uppy/core' +import { UIPlugin, type UIPluginOptions } from '@uppy/core' +import { FilterInput, SearchView } from '@uppy/provider-views' +import { + Assembly, + type AssemblyResult, + Client, + type OptionsWithRestructuredFields, +} from '@uppy/transloadit' +import { RateLimitedQueue } from '@uppy/utils' +import type { h } from 'preact' +import locale from './locale.js' + +export interface ImageGeneratorOptions extends UIPluginOptions { + // OptionsWithRestructuredFields does not allow string[] for `fields`. + // in @uppy/transloadit we do accept that but then immediately use a type assertion to this type + // so that's why we just don't allow string[] from the start here + assemblyOptions: (prompt: string) => Promise +} + +interface PluginState extends Record { + prompt: string + results: AssemblyResult[] + checkedResultIds: Set + loading: boolean + loadingMessageIndex: number + firstRun: boolean +} + +const defaultState = { + prompt: '', + results: [], + checkedResultIds: new Set(), + loading: false, + loadingMessageIndex: 0, + firstRun: true, +} satisfies PluginState + +const LOADING_MESSAGES = [ + 'generating1', + 'generating2', + 'generating3', + 'generating4', + 'generating5', +] as const + +export default class ImageGenerator< + M extends Meta, + B extends Body, +> extends UIPlugin { + private loadingInterval: ReturnType | null = null + private rateLimitedQueue: RateLimitedQueue + private client: Client + private assembly: Assembly | null = null + icon: () => h.JSX.Element + + constructor(uppy: Uppy, opts: ImageGeneratorOptions) { + super(uppy, opts) + + this.id = this.opts.id || 'ImageGenerator' + this.title = 'AI image' + this.type = 'acquirer' + + this.defaultLocale = locale + + this.rateLimitedQueue = new RateLimitedQueue(10) + this.client = new Client({ + service: 'https://api2.transloadit.com', + rateLimitedQueue: this.rateLimitedQueue, + errorReporting: true, + }) + + this.setPluginState(defaultState) + + this.i18nInit() + + this.icon = () => ( + + ai-image-generator + + + + + + + + + + + + + ) + } + + install(): void { + const { target } = this.opts + if (target) { + this.mount(target, this) + } + } + + uninstall(): void { + this.clearLoadingInterval() + this.closeAssembly(true) // Cancel any in-progress assembly + this.unmount() + } + + private closeAssembly(cancel = false): void { + if (this.assembly) { + const { status } = this.assembly + this.assembly.close() + this.assembly = null + + // Cancel the assembly on the server to stop processing + if (cancel && status) { + this.client.cancelAssembly(status).catch(() => { + // If we can't cancel, there's not much we can do + }) + } + } + } + + private clearLoadingInterval(): void { + if (this.loadingInterval) { + clearInterval(this.loadingInterval) + this.loadingInterval = null + } + } + + private startLoadingAnimation(): void { + this.clearLoadingInterval() + this.loadingInterval = setInterval(() => { + const { loadingMessageIndex } = this.getPluginState() + const nextIndex = (loadingMessageIndex + 1) % LOADING_MESSAGES.length + this.setPluginState({ loadingMessageIndex: nextIndex }) + }, 4000) + } + + /** + * Creates a Transloadit assembly to generate AI images. + * + * Completion scenarios: + * - Success: assembly emits 'finished' → resolve() → finally cleans up, keeps results + * - Error: assembly emits 'error' → reject() → catch reports error, finally cleans up + * - Dashboard close: onDashboardClose sets cancelled=true, resolve() → finally resets state + * - Uninstall: closeAssembly(true) called directly, cancels server-side assembly + */ + generate = async () => { + const { loading, prompt } = this.getPluginState() + if (loading || prompt.trim() === '') return + + const { promise, resolve, reject } = Promise.withResolvers() + let cancelled = false + + const onDashboardClose = () => { + cancelled = true + resolve() + } + + // @ts-expect-error not typed because we do not depend on @uppy/dashboard + this.uppy.once('dashboard:close-panel', onDashboardClose) + + try { + this.setPluginState({ + loading: true, + results: [], + checkedResultIds: new Set(), + loadingMessageIndex: 0, + }) + this.startLoadingAnimation() + + const assemblyOptions = await this.opts.assemblyOptions( + this.getPluginState().prompt, + ) + + const assemblyResponse = await this.client.createAssembly({ + params: assemblyOptions.params, + fields: assemblyOptions.fields ?? {}, + signature: assemblyOptions.signature, + expectedFiles: 0, + }) + + const assembly = new Assembly(assemblyResponse, this.rateLimitedQueue) + this.assembly = assembly + + assembly.on('result', (stepName: string, result: AssemblyResult) => { + const { results } = this.getPluginState() + this.setPluginState({ + results: [...results, result], + firstRun: false, + }) + }) + + assembly.on('error', reject) + assembly.on('finished', resolve) + assembly.connect() + + await promise + } catch (error) { + this.client.submitError(error as Error).catch(() => {}) + this.uppy.info('Image could not be generated', 'error') + throw error + } finally { + // @ts-expect-error not typed because we do not depend on @uppy/dashboard + this.uppy.off('dashboard:close-panel', onDashboardClose) + this.clearLoadingInterval() + this.closeAssembly(true) + this.setPluginState(cancelled ? defaultState : { loading: false }) + } + } + + private onCheckboxChange = (result: AssemblyResult, event?: Event) => { + if (event) { + event.stopPropagation() + event.preventDefault() + // Prevent shift-clicking from highlighting file names + document.getSelection()?.removeAllRanges() + } + + const { checkedResultIds } = this.getPluginState() + + if (checkedResultIds.has(result.id)) { + checkedResultIds.delete(result.id) + } else { + checkedResultIds.add(result.id) + } + + this.setPluginState({ checkedResultIds }) + } + + private cancelSelection = () => { + this.setPluginState({ checkedResultIds: new Set() }) + } + + private donePicking = async () => { + const { checkedResultIds, results } = this.getPluginState() + const proms: Promise>[] = results + .filter((result) => checkedResultIds.has(result.id)) + .map(async (result) => { + const res = await fetch(result.ssl_url!) + const blob = await res.blob() + + return { + name: `ai-image-${result.id!}`, + type: result.mime ?? undefined, + source: 'Transloadit', + data: blob, + } + }) + const files = await Promise.all(proms) + + this.uppy.addFiles(files) + this.setPluginState(defaultState) + } + + render() { + const { + prompt, + results, + checkedResultIds, + loading, + loadingMessageIndex, + firstRun, + } = this.getPluginState() + const { i18n } = this.uppy + + const currentLoadingMessage = loading + ? i18n(LOADING_MESSAGES[loadingMessageIndex]) + : undefined + + if (firstRun) { + return ( + this.setPluginState({ prompt })} + onSubmit={this.generate} + inputLabel={i18n('generateImagePlaceholder')} + loading={loading} + > + {loading ? ( + + {currentLoadingMessage} + + ) : ( + i18n('generateImage') + )} + + ) + } + + return ( +
+ this.setPluginState({ prompt })} + onSubmit={this.generate} + inputLabel={i18n('search')} + i18n={i18n} + /> + + {loading ? ( +
+ {currentLoadingMessage} +
+ ) : results.length > 0 ? ( +
+
    + {results.map((result) => ( +
  • + this.onCheckboxChange(result, e)} + name="listitem" + id={result.id} + checked={checkedResultIds.has(result.id)} + data-uppy-super-focusable + /> + +
  • + ))} +
+
+ ) : ( +
{i18n('noSearchResults')}
+ )} + + {checkedResultIds.size > 0 && ( +
+
+ + +
+
+ )} +
+ ) + } +} diff --git a/packages/@uppy/image-generator/src/locale.ts b/packages/@uppy/image-generator/src/locale.ts new file mode 100644 index 000000000..39881d7cd --- /dev/null +++ b/packages/@uppy/image-generator/src/locale.ts @@ -0,0 +1,12 @@ +export default { + strings: { + generateImage: 'Generate image', + generateImagePlaceholder: + 'A serene sunset over a mountain lake, with pine trees reflecting in the water', + generating1: 'AI is thinking...', + generating2: 'Crunching pixels...', + generating3: 'Summoning images...', + generating4: 'AI is working...', + generating5: 'Creating magic...', + }, +} diff --git a/packages/@uppy/image-generator/src/style.scss b/packages/@uppy/image-generator/src/style.scss new file mode 100644 index 000000000..1053cf2e9 --- /dev/null +++ b/packages/@uppy/image-generator/src/style.scss @@ -0,0 +1,29 @@ +@use '@uppy/core/src/_variables.scss' as *; + +.uppy-ImageGenerator-generating { + background: linear-gradient(90deg, $gray-50, $gray-300, $gray-50); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: uppy-gradient-slide 3s ease-in-out infinite; +} + +.uppy-ImageGenerator-generating--darker { + background: linear-gradient(90deg, $gray-500, $gray-800, $gray-500); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: uppy-gradient-slide 3s ease-in-out infinite; +} + +@keyframes uppy-gradient-slide { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } +} diff --git a/packages/@uppy/image-generator/tsconfig.build.json b/packages/@uppy/image-generator/tsconfig.build.json new file mode 100644 index 000000000..389eb1387 --- /dev/null +++ b/packages/@uppy/image-generator/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src" + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/image-generator/tsconfig.json b/packages/@uppy/image-generator/tsconfig.json new file mode 100644 index 000000000..ee7d9ca9f --- /dev/null +++ b/packages/@uppy/image-generator/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + }, + { + "path": "../provider-views/tsconfig.build.json" + }, + { + "path": "../transloadit/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/image-generator/turbo.json b/packages/@uppy/image-generator/turbo.json new file mode 100644 index 000000000..c7d5acc18 --- /dev/null +++ b/packages/@uppy/image-generator/turbo.json @@ -0,0 +1,8 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "dependsOn": ["^build", "@uppy/core#build", "@uppy/transloadit#build"] + } + } +} diff --git a/packages/@uppy/locales/CHANGELOG.md b/packages/@uppy/locales/CHANGELOG.md index 7ee9605c5..745d1c392 100644 --- a/packages/@uppy/locales/CHANGELOG.md +++ b/packages/@uppy/locales/CHANGELOG.md @@ -1,5 +1,11 @@ # @uppy/locales +## 5.1.0 + +### Minor Changes + +- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit + ## 5.0.1 ### Patch Changes diff --git a/packages/@uppy/locales/package.json b/packages/@uppy/locales/package.json index 996e66291..a9d908b85 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": "5.0.1", + "version": "5.1.0", "license": "MIT", "type": "module", "sideEffects": false, diff --git a/packages/@uppy/locales/src/en_US.ts b/packages/@uppy/locales/src/en_US.ts index fd8925bb9..cca5feb7b 100644 --- a/packages/@uppy/locales/src/en_US.ts +++ b/packages/@uppy/locales/src/en_US.ts @@ -44,6 +44,7 @@ en_US.strings = { browseFolders: 'browse folders', cancel: 'Cancel', cancelUpload: 'Cancel upload', + chooseFiles: 'Choose files', closeModal: 'Close Modal', companionError: 'Connection with Companion failed', companionUnauthorizeHint: @@ -100,6 +101,14 @@ en_US.strings = { '1': 'Added %{smart_count} files from %{folder}', }, folderAlreadyAdded: 'The folder "%{folder}" was already added', + generateImage: 'Generate image', + generateImagePlaceholder: + 'A serene sunset over a mountain lake, with pine trees reflecting in the water', + generating1: 'AI is thinking...', + generating2: 'Crunching pixels...', + generating3: 'Summoning images...', + generating4: 'AI is working...', + generating5: 'Creating magic...', generatingThumbnails: 'Generating thumbnails...', import: 'Import', importFiles: 'Import files from:', diff --git a/packages/@uppy/locales/turbo.json b/packages/@uppy/locales/turbo.json index 734ebe0e5..7c1a5e518 100644 --- a/packages/@uppy/locales/turbo.json +++ b/packages/@uppy/locales/turbo.json @@ -4,6 +4,7 @@ "build": { "dependsOn": [ "@uppy/image-editor#build", + "@uppy/image-generator#build", "@uppy/box#build", "@uppy/core#build", "@uppy/google-drive-picker#build", diff --git a/packages/@uppy/provider-views/CHANGELOG.md b/packages/@uppy/provider-views/CHANGELOG.md index ea834ec83..c939b7165 100644 --- a/packages/@uppy/provider-views/CHANGELOG.md +++ b/packages/@uppy/provider-views/CHANGELOG.md @@ -1,5 +1,11 @@ # @uppy/provider-views +## 5.2.1 + +### Patch Changes + +- 5684efa: Refactor internal components + ## 5.2.0 ### Minor Changes diff --git a/packages/@uppy/provider-views/package.json b/packages/@uppy/provider-views/package.json index 4a4915749..40de2f76b 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": "5.2.0", + "version": "5.2.1", "license": "MIT", "style": "dist/style.min.css", "type": "module", diff --git a/packages/@uppy/provider-views/src/FilterInput.tsx b/packages/@uppy/provider-views/src/FilterInput.tsx new file mode 100644 index 000000000..1fa9baabe --- /dev/null +++ b/packages/@uppy/provider-views/src/FilterInput.tsx @@ -0,0 +1,68 @@ +import type { I18n } from '@uppy/utils' +import { useSearchForm } from './useSearchForm.js' + +interface FilterInputProps { + value: string + onChange: (value: string) => void + onSubmit: () => void + inputLabel: string + i18n: I18n +} + +/** + * FilterInput component for client-side filtering with search icon and clear button. + * Supports Enter key submission via form element. + */ +function FilterInput({ + value, + onChange, + onSubmit, + inputLabel, + i18n, +}: FilterInputProps) { + const { formId } = useSearchForm(onSubmit) + + return ( +
+ onChange((e.target as HTMLInputElement).value)} + form={formId} + data-uppy-super-focusable + /> + + {value && ( + + )} +
+ ) +} + +export default FilterInput diff --git a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx index b231340e9..4e010678e 100644 --- a/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx +++ b/packages/@uppy/provider-views/src/ProviderView/ProviderView.tsx @@ -17,8 +17,8 @@ import debounce from 'lodash/debounce.js' import type { h } from 'preact' import packageJson from '../../package.json' with { type: 'json' } import Browser from '../Browser.js' +import FilterInput from '../FilterInput.js' import FooterActions from '../FooterActions.js' -import SearchInput from '../SearchInput.js' import addFiles from '../utils/addFiles.js' import getClickedRange from '../utils/getClickedRange.js' import handleError from '../utils/handleError.js' @@ -678,14 +678,12 @@ export default class ProviderView { i18n={i18n} /> {opts.showFilter && ( - this.onSearchInput(s)} - submitSearchString={() => {}} + this.onSearchInput(s)} + onSubmit={() => {}} inputLabel={i18n('filter')} - clearSearchLabel={i18n('resetFilter')} - wrapperClassName="uppy-ProviderBrowser-searchFilter" - inputClassName="uppy-ProviderBrowser-searchFilterInput" + i18n={i18n} /> )} diff --git a/packages/@uppy/provider-views/src/SearchInput.tsx b/packages/@uppy/provider-views/src/SearchInput.tsx deleted file mode 100644 index a95622799..000000000 --- a/packages/@uppy/provider-views/src/SearchInput.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { nanoid } from 'nanoid/non-secure' -import { useCallback, useEffect, useState } from 'preact/hooks' - -type Props = { - searchString: string - setSearchString: (s: string) => void - submitSearchString: () => void - - wrapperClassName: string - inputClassName: string - - inputLabel: string - clearSearchLabel?: string - - showButton?: boolean - buttonLabel?: string - buttonCSSClassName?: string -} - -function SearchInput({ - searchString, - setSearchString, - submitSearchString, - - wrapperClassName, - inputClassName, - - inputLabel, - clearSearchLabel = '', - - showButton = false, - buttonLabel = '', - buttonCSSClassName = '', -}: Props) { - const onInput = (e: Event) => { - setSearchString((e.target as HTMLInputElement).value) - } - - const submit = useCallback( - (ev: Event) => { - ev.preventDefault() - submitSearchString() - }, - [submitSearchString], - ) - - // We do this to avoid nested
s - // (See https://github.com/transloadit/uppy/pull/5050#discussion_r1640392516) - const [form] = useState(() => { - const formEl = document.createElement('form') - formEl.setAttribute('tabindex', '-1') - formEl.id = nanoid() - return formEl - }) - - useEffect(() => { - document.body.appendChild(form) - form.addEventListener('submit', submit) - return () => { - form.removeEventListener('submit', submit) - document.body.removeChild(form) - } - }, [form, submit]) - - return ( -
- - {!showButton && ( - // 🔍 - - )} - {!showButton && searchString && ( - // ❌ - - )} - {showButton && ( - - )} -
- ) -} - -export default SearchInput diff --git a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx index 8de25d1fe..2de72ba92 100644 --- a/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx +++ b/packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.tsx @@ -16,8 +16,9 @@ import classNames from 'classnames' import type { h } from 'preact' import packageJson from '../../package.json' with { type: 'json' } import Browser from '../Browser.js' +import FilterInput from '../FilterInput.js' import FooterActions from '../FooterActions.js' -import SearchInput from '../SearchInput.js' +import SearchView from '../SearchView.js' import addFiles from '../utils/addFiles.js' import getClickedRange from '../utils/getClickedRange.js' import handleError from '../utils/handleError.js' @@ -278,17 +279,14 @@ export default class SearchProviderView { if (isInputMode) { return ( - + > + {i18n('searchImages')} + ) } @@ -300,14 +298,12 @@ export default class SearchProviderView { )} > {opts.showFilter && ( - )} diff --git a/packages/@uppy/provider-views/src/SearchView.tsx b/packages/@uppy/provider-views/src/SearchView.tsx new file mode 100644 index 000000000..4f25b7b84 --- /dev/null +++ b/packages/@uppy/provider-views/src/SearchView.tsx @@ -0,0 +1,53 @@ +import type { ComponentChild } from 'preact' +import { useSearchForm } from './useSearchForm.js' + +interface SearchViewProps { + value: string + onChange: (value: string) => void + onSubmit: () => void + inputLabel: string + loading?: boolean + children: ComponentChild +} + +/** + * SearchView component for search with a submit button. + * Typically used for initial search views or forms that require explicit submission. + * The children prop is rendered as the button content, allowing full control over button text and loading states. + */ +function SearchView({ + value, + onChange, + onSubmit, + inputLabel, + loading = false, + children, +}: SearchViewProps) { + const { formId } = useSearchForm(onSubmit) + + return ( +
+ onChange((e.target as HTMLInputElement).value)} + form={formId} + disabled={loading} + data-uppy-super-focusable + /> + +
+ ) +} + +export default SearchView diff --git a/packages/@uppy/provider-views/src/index.ts b/packages/@uppy/provider-views/src/index.ts index 08cfcb498..6d23bb688 100644 --- a/packages/@uppy/provider-views/src/index.ts +++ b/packages/@uppy/provider-views/src/index.ts @@ -1,3 +1,4 @@ +export { default as FilterInput } from './FilterInput.js' export { default as GooglePickerView } from './GooglePicker/GooglePickerView.js' export type { MediaItem, @@ -24,5 +25,6 @@ export { default as ProviderViews, defaultPickerIcon, } from './ProviderView/index.js' -export { default as SearchInput } from './SearchInput.js' export { default as SearchProviderViews } from './SearchProviderView/index.js' +export { default as SearchView } from './SearchView.js' +export { useSearchForm } from './useSearchForm.js' diff --git a/packages/@uppy/provider-views/src/useSearchForm.ts b/packages/@uppy/provider-views/src/useSearchForm.ts new file mode 100644 index 000000000..2ca162744 --- /dev/null +++ b/packages/@uppy/provider-views/src/useSearchForm.ts @@ -0,0 +1,42 @@ +import { nanoid } from 'nanoid/non-secure' +import { useCallback, useEffect, useState } from 'preact/hooks' + +/** + * Hook to create a form element outside the component tree to avoid nested forms. + * Returns a formId that can be used with the `form` attribute on inputs and buttons. + * + * This allows form submission (Enter key) to work properly even when the component + * is rendered inside another form element. + * + * @param onSubmit - Callback to execute when the form is submitted + * @returns Object containing the formId to use with form attribute + */ +export function useSearchForm(onSubmit: () => void): { formId: string } { + const submit = useCallback( + (ev: Event) => { + ev.preventDefault() + onSubmit() + }, + [onSubmit], + ) + + // We create a form element and append it to document.body to avoid nested s + // (See https://github.com/transloadit/uppy/pull/5050#discussion_r1640392516) + const [form] = useState(() => { + const formEl = document.createElement('form') + formEl.setAttribute('tabindex', '-1') + formEl.id = nanoid() + return formEl + }) + + useEffect(() => { + document.body.appendChild(form) + form.addEventListener('submit', submit) + return () => { + form.removeEventListener('submit', submit) + document.body.removeChild(form) + } + }, [form, submit]) + + return { formId: form.id } +} diff --git a/packages/@uppy/transloadit/CHANGELOG.md b/packages/@uppy/transloadit/CHANGELOG.md index d1789060b..86c8cf5ee 100644 --- a/packages/@uppy/transloadit/CHANGELOG.md +++ b/packages/@uppy/transloadit/CHANGELOG.md @@ -1,5 +1,11 @@ # @uppy/transloadit +## 5.4.0 + +### Minor Changes + +- 5684efa: Export Assembly, AssemblyError, Client + ## 5.3.0 ### Minor Changes diff --git a/packages/@uppy/transloadit/package.json b/packages/@uppy/transloadit/package.json index cc9ce7eba..78e842609 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": "5.3.0", + "version": "5.4.0", "license": "MIT", "type": "module", "sideEffects": false, diff --git a/packages/@uppy/transloadit/src/index.ts b/packages/@uppy/transloadit/src/index.ts index 21544ab52..9a628570a 100644 --- a/packages/@uppy/transloadit/src/index.ts +++ b/packages/@uppy/transloadit/src/index.ts @@ -819,7 +819,9 @@ export default class Transloadit< : this.opts.assemblyOptions ) as OptionsWithRestructuredFields - assemblyOptions.fields ??= {} + assemblyOptions.fields = { + ...(assemblyOptions.fields ?? {}), + } validateParams(assemblyOptions.params) try { @@ -1032,3 +1034,7 @@ export default class Transloadit< } export { COMPANION_URL, COMPANION_ALLOWED_HOSTS } + +// Low-level classes for advanced usage (e.g., creating assemblies without file uploads) +export { default as Assembly } from './Assembly.js' +export { AssemblyError, default as Client } from './Client.js' diff --git a/packages/@uppy/webdav/CHANGELOG.md b/packages/@uppy/webdav/CHANGELOG.md index f153307db..cd2f2b18b 100644 --- a/packages/@uppy/webdav/CHANGELOG.md +++ b/packages/@uppy/webdav/CHANGELOG.md @@ -1,5 +1,13 @@ # @uppy/webdav +## 1.1.1 + +### Patch Changes + +- 5684efa: Refactor internal components +- Updated dependencies [5684efa] + - @uppy/provider-views@5.2.1 + ## 1.1.0 ### Minor Changes diff --git a/packages/@uppy/webdav/package.json b/packages/@uppy/webdav/package.json index 7c241ee1c..754ef5423 100644 --- a/packages/@uppy/webdav/package.json +++ b/packages/@uppy/webdav/package.json @@ -1,7 +1,7 @@ { "name": "@uppy/webdav", "description": "Import files from WebDAV into Uppy.", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "types": "types/index.d.ts", "type": "module", diff --git a/packages/@uppy/webdav/src/Webdav.tsx b/packages/@uppy/webdav/src/Webdav.tsx index acf6e12df..840abe605 100644 --- a/packages/@uppy/webdav/src/Webdav.tsx +++ b/packages/@uppy/webdav/src/Webdav.tsx @@ -14,7 +14,7 @@ import type { } from '@uppy/core' import { UIPlugin } from '@uppy/core' -import { ProviderViews, SearchInput } from '@uppy/provider-views' +import { ProviderViews, SearchView } from '@uppy/provider-views' import type { I18n, LocaleStrings } from '@uppy/utils' // biome-ignore lint/style/useImportType: h is not a type import { type ComponentChild, h } from 'preact' @@ -61,17 +61,14 @@ const AuthForm = ({ }, [onAuth, webdavUrl]) return ( - + > + {i18n('authenticate')} + ) } diff --git a/packages/uppy/CHANGELOG.md b/packages/uppy/CHANGELOG.md index 6fdc4be17..403b8f127 100644 --- a/packages/uppy/CHANGELOG.md +++ b/packages/uppy/CHANGELOG.md @@ -1,5 +1,22 @@ # uppy +## 5.2.0 + +### Minor Changes + +- 5684efa: Introduce @uppy/image-generator to generate images based on a prompt using Transloadit + +### Patch Changes + +- Updated dependencies [5684efa] +- Updated dependencies [5684efa] +- Updated dependencies [5684efa] + - @uppy/provider-views@5.2.1 + - @uppy/webdav@1.1.1 + - @uppy/transloadit@5.4.0 + - @uppy/image-generator@1.0.0 + - @uppy/locales@5.1.0 + ## 5.1.12 ### Patch Changes diff --git a/packages/uppy/package.json b/packages/uppy/package.json index d53696e36..ad37bdb75 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": "5.1.12", + "version": "5.2.0", "license": "MIT", "main": "lib/index.js", "module": "lib/index.js", @@ -54,6 +54,7 @@ "@uppy/google-drive-picker": "workspace:*", "@uppy/google-photos-picker": "workspace:*", "@uppy/image-editor": "workspace:*", + "@uppy/image-generator": "workspace:*", "@uppy/instagram": "workspace:*", "@uppy/locales": "workspace:*", "@uppy/onedrive": "workspace:*", diff --git a/packages/uppy/src/style.scss b/packages/uppy/src/style.scss index 99baf3f24..6b331ddd8 100644 --- a/packages/uppy/src/style.scss +++ b/packages/uppy/src/style.scss @@ -8,4 +8,5 @@ @use '@uppy/screen-capture/src/style.scss' as screen-capture; @use '@uppy/status-bar/src/style.scss' as status-bar; @use '@uppy/image-editor/src/style.scss' as image-editor; +@use '@uppy/image-generator/src/style.scss' as image-generator; @use '@uppy/drop-target/src/style.scss' as drop-target; diff --git a/private/dev/Dashboard.js b/private/dev/Dashboard.js index 456e457ab..f3b7ffdd5 100644 --- a/private/dev/Dashboard.js +++ b/private/dev/Dashboard.js @@ -10,6 +10,7 @@ import GoogleDrive from '@uppy/google-drive' import GoogleDrivePicker from '@uppy/google-drive-picker' import GooglePhotosPicker from '@uppy/google-photos-picker' import ImageEditor from '@uppy/image-editor' +import ImageGenerator from '@uppy/image-generator' import english from '@uppy/locales/lib/en_US.js' import RemoteSources from '@uppy/remote-sources' import ScreenCapture from '@uppy/screen-capture' @@ -174,6 +175,25 @@ export default () => { .use(ScreenCapture, { target: Dashboard }) .use(Form, { target: '#upload-form' }) .use(ImageEditor, { target: Dashboard }) + .use(ImageGenerator, { + target: Dashboard, + assemblyOptions: async (prompt) => + // never create a signature on the client in production! + // it will expose the secret on the client + generateSignatureIfSecret(TRANSLOADIT_SECRET, { + auth: { key: TRANSLOADIT_KEY }, + steps: { + generated_image: { + robot: '/image/generate', + result: true, + aspect_ratio: '2:3', + model: 'flux-1.1-pro-ultra', + prompt, + num_outputs: 2, + }, + }, + }), + }) .use(DropTarget, { target: document.body, }) diff --git a/turbo.json b/turbo.json index 0fec0fb4f..687e96dd8 100644 --- a/turbo.json +++ b/turbo.json @@ -23,6 +23,12 @@ ], "outputs": ["lib/**", "dist/**"] }, + "uppy#build:css": { + "outputLogs": "new-only", + "dependsOn": ["^build:css"], + "inputs": ["src/**/*.scss"], + "outputs": ["lib/**/*.css", "dist/**/*.css"] + }, "build:css": { "outputLogs": "new-only", "inputs": ["src/**/*.scss"], diff --git a/yarn.lock b/yarn.lock index b00a94c96..c303f7719 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4958,65 +4958,65 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.5.2": - version: 15.5.2 - resolution: "@next/env@npm:15.5.2" - checksum: 10/1e1c4f5b725165663460bae67de95cab624c66a865395a0af98405d3302483bebed8e79fcc2dc1c447b7010b5519fddb49670de16b00b75d679b52b29a4d86f5 +"@next/env@npm:15.5.7": + version: 15.5.7 + resolution: "@next/env@npm:15.5.7" + checksum: 10/11f971691018bd62a5bf253fc843fb2a6cf1431468f5c3a9d4d41753a6ff3e8bf7f539f46aba3f58f8bac59e681bf05fb5d771ac08d7dbd966a601257e1368bf languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-darwin-arm64@npm:15.5.2" +"@next/swc-darwin-arm64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-arm64@npm:15.5.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-darwin-x64@npm:15.5.2" +"@next/swc-darwin-x64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-x64@npm:15.5.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-linux-arm64-gnu@npm:15.5.2" +"@next/swc-linux-arm64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-linux-arm64-musl@npm:15.5.2" +"@next/swc-linux-arm64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-linux-x64-gnu@npm:15.5.2" +"@next/swc-linux-x64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-linux-x64-musl@npm:15.5.2" +"@next/swc-linux-x64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-musl@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-win32-arm64-msvc@npm:15.5.2" +"@next/swc-win32-arm64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.5.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.5.2": - version: 15.5.2 - resolution: "@next/swc-win32-x64-msvc@npm:15.5.2" +"@next/swc-win32-x64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.5.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9139,6 +9139,24 @@ __metadata: languageName: unknown linkType: soft +"@uppy/image-generator@workspace:*, @uppy/image-generator@workspace:packages/@uppy/image-generator": + version: 0.0.0-use.local + resolution: "@uppy/image-generator@workspace:packages/@uppy/image-generator" + dependencies: + "@uppy/core": "workspace:^" + "@uppy/provider-views": "workspace:^" + "@uppy/transloadit": "workspace:^" + "@uppy/utils": "workspace:^" + cssnano: "npm:^7.0.7" + postcss: "npm:^8.5.6" + postcss-cli: "npm:^11.0.1" + preact: "npm:^10.5.13" + sass: "npm:^1.89.2" + peerDependencies: + "@uppy/core": "workspace:^" + languageName: unknown + linkType: soft + "@uppy/instagram@workspace:*, @uppy/instagram@workspace:^, @uppy/instagram@workspace:packages/@uppy/instagram": version: 0.0.0-use.local resolution: "@uppy/instagram@workspace:packages/@uppy/instagram" @@ -9362,7 +9380,7 @@ __metadata: languageName: unknown linkType: soft -"@uppy/transloadit@workspace:*, @uppy/transloadit@workspace:packages/@uppy/transloadit": +"@uppy/transloadit@workspace:*, @uppy/transloadit@workspace:^, @uppy/transloadit@workspace:packages/@uppy/transloadit": version: 0.0.0-use.local resolution: "@uppy/transloadit@workspace:packages/@uppy/transloadit" dependencies: @@ -13268,7 +13286,7 @@ __metadata: "@uppy/transloadit": "workspace:*" "@uppy/tus": "workspace:*" "@uppy/xhr-upload": "workspace:*" - next: "npm:15.5.2" + next: "npm:15.5.7" react: "npm:19.1.0" react-dom: "npm:19.1.0" tailwindcss: "npm:^4" @@ -17660,19 +17678,19 @@ __metadata: languageName: node linkType: hard -"next@npm:15.5.2": - version: 15.5.2 - resolution: "next@npm:15.5.2" +"next@npm:15.5.7": + version: 15.5.7 + resolution: "next@npm:15.5.7" dependencies: - "@next/env": "npm:15.5.2" - "@next/swc-darwin-arm64": "npm:15.5.2" - "@next/swc-darwin-x64": "npm:15.5.2" - "@next/swc-linux-arm64-gnu": "npm:15.5.2" - "@next/swc-linux-arm64-musl": "npm:15.5.2" - "@next/swc-linux-x64-gnu": "npm:15.5.2" - "@next/swc-linux-x64-musl": "npm:15.5.2" - "@next/swc-win32-arm64-msvc": "npm:15.5.2" - "@next/swc-win32-x64-msvc": "npm:15.5.2" + "@next/env": "npm:15.5.7" + "@next/swc-darwin-arm64": "npm:15.5.7" + "@next/swc-darwin-x64": "npm:15.5.7" + "@next/swc-linux-arm64-gnu": "npm:15.5.7" + "@next/swc-linux-arm64-musl": "npm:15.5.7" + "@next/swc-linux-x64-gnu": "npm:15.5.7" + "@next/swc-linux-x64-musl": "npm:15.5.7" + "@next/swc-win32-arm64-msvc": "npm:15.5.7" + "@next/swc-win32-x64-msvc": "npm:15.5.7" "@swc/helpers": "npm:0.5.15" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" @@ -17715,7 +17733,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/0e5a7420e7ea9bba57b32575378c9d74eb4cc95a34453d0e88dd6212f95d7d4f9fd35a106dba0ca5f537e3265a3ab48cf93487f7c792502ee83da3a9bb4be92c + checksum: 10/bfac0cbac41b36227ec91d3a0727561f73dfc35d6a78eafd0200528ef95fa2e9d2dba5a8c8922864b4f4e4321ae6a07c6cf8f5766ac3192ad03e68516c3a458a languageName: node linkType: hard @@ -22649,6 +22667,7 @@ __metadata: "@uppy/google-drive-picker": "workspace:*" "@uppy/google-photos-picker": "workspace:*" "@uppy/image-editor": "workspace:*" + "@uppy/image-generator": "workspace:*" "@uppy/instagram": "workspace:*" "@uppy/locales": "workspace:*" "@uppy/onedrive": "workspace:*"