mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
Introduce @uppy/image-generator (#6056)
Closes #5378
- Introduce `@uppy/image-generator`, a new plugin to generate images
based on a prompt via Transloadit
- until we have "golden templates" the idea is to just send
[steps](https://transloadit.com/docs/topics/templates/#overruling-templates-at-runtime)
- because we must send steps and since we must use signature
authentication for security, which is signed based on the params we
send, we can't reuse the `assemblyOptions` the consumers is already
passing to `@uppy/transloadit` (if they use that uploaders, not needed).
- Remove `SearchInput` (this component was trying to be too many things,
all with conditional boolean props, which is bad practise) in favor of
`useSearchForm` and reuse this hook in two new components `SearchView`
and `FilterInput`
- Reuse all the styles from `SearchProviderView`. This deviates from the
design in #5378. It felt too inconsistent to me to do another UI here
again. For the initial version, I think it's best to stay consistent and
then redesign with search providers taken into account too.
- Because the service is so slow, I went a bit further with the loading
state to show funny messages that rotate while loading mostly because
users will start thinking it is broken after 5 seconds while it fact we
are still loading. But open to ideas here.
This unfortunately means the integration for the consumer is not as lean
and pretty as you would hope. On the upside, it does give them complete
freedom.
```ts
.use(ImageGenerator, {
assemblyOptions: async (prompt) => {
const res = await fetch(`/assembly-options?prompt=${encodeURIComponent(prompt)}`)
return res.json()
}
})
```
on the consumer's server:
```ts
import crypto from 'node:crypto'
const utcDateString = (ms) => {
return new Date(ms)
.toISOString()
.replace(/-/g, '/')
.replace(/T/, ' ')
.replace(/\.\d+Z$/, '+00:00')
}
// expire 1 hour from now (this must be milliseconds)
const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000)
const authKey = 'YOUR_TRANSLOADIT_KEY'
const authSecret = 'YOUR_TRANSLOADIT_SECRET'
const params = JSON.stringify({
auth: {
key: authKey,
expires,
},
// can not contain any more steps, the only step must be /image/generate
steps: {
generated_image: { // can be named different
robot: '/image/generate',
result: true, // mandatory
aspect_ratio: '2:3', // up to them
model: 'flux-1.1-pro-ultra', // up to them
prompt, // mandatory
num_outputs: 2, // up to them
},
},
})
const signatureBytes = crypto.createHmac('sha384', authSecret).update(Buffer.from(params, 'utf-8'))
// The final signature needs the hash name in front, so
// the hashing algorithm can be updated in a backwards-compatible
// way when old algorithms become insecure.
const signature = `sha384:${signatureBytes.digest('hex')}`
// respond with { params, signature } JSON to the client
```
https://github.com/user-attachments/assets/9217e457-b38b-48ac-81f0-37a417309e98
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> Adds AI image generation plugin using Transloadit, exports low-level
Transloadit APIs, and replaces SearchInput with new
FilterInput/SearchView + useSearchForm across provider views.
>
> - **New plugin: `@uppy/image-generator`**
> - UI plugin to generate images from a prompt via Transloadit
(`src/index.tsx`, styles, locale, build configs).
> - Integrated into dev Dashboard and included in `uppy` bundle and
global styles.
> - **Provider Views refactor**
> - Remove `SearchInput`; introduce `useSearchForm`, `SearchView`, and
`FilterInput` components.
> - Update `ProviderView`, `SearchProviderView`, and `Webdav` to use new
components; export them from `@uppy/provider-views`.
> - **Transloadit updates**
> - Export `Assembly`, `AssemblyError`, and `Client` from
`@uppy/transloadit`.
> - Minor internal change: normalize `assemblyOptions.fields`.
> - **Locales**
> - Add strings for image generation and minor additions (e.g.,
`chooseFiles`).
> - Ensure locales build depends on `@uppy/image-generator`.
> - **Build config**
> - Turborepo: add `uppy#build:css` and hook `image-generator` into
locales build.
> - **Changesets**
> - `@uppy/image-generator` major; `@uppy/transloadit` minor;
`@uppy/locales` and `uppy` minor; `@uppy/provider-views` and
`@uppy/webdav` patch.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4b1b729069. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Prakash <qxprakash@gmail.com>
This commit is contained in:
parent
93ef1ba0e7
commit
5684efa64e
28 changed files with 895 additions and 205 deletions
6
.changeset/afraid-seas-scream.md
Normal file
6
.changeset/afraid-seas-scream.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@uppy/provider-views": patch
|
||||
"@uppy/webdav": patch
|
||||
---
|
||||
|
||||
Refactor internal components
|
||||
5
.changeset/swift-flies-dance.md
Normal file
5
.changeset/swift-flies-dance.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/transloadit": minor
|
||||
---
|
||||
|
||||
Export Assembly, AssemblyError, Client
|
||||
7
.changeset/twelve-results-smoke.md
Normal file
7
.changeset/twelve-results-smoke.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@uppy/image-generator": major
|
||||
"@uppy/locales": minor
|
||||
"uppy": minor
|
||||
---
|
||||
|
||||
Introduce @uppy/image-generator to generate images based on a prompt using Transloadit
|
||||
21
packages/@uppy/image-generator/LICENSE
Normal file
21
packages/@uppy/image-generator/LICENSE
Normal file
|
|
@ -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.
|
||||
49
packages/@uppy/image-generator/README.md
Normal file
49
packages/@uppy/image-generator/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# @uppy/image-generator
|
||||
|
||||
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
|
||||
|
||||
[](https://www.npmjs.com/package/@uppy/image-generator)
|
||||

|
||||

|
||||

|
||||
|
||||
**[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).
|
||||
60
packages/@uppy/image-generator/package.json
Normal file
60
packages/@uppy/image-generator/package.json
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "@uppy/image-generator",
|
||||
"description": "AI-powered image generation for Uppy",
|
||||
"version": "0.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"
|
||||
}
|
||||
}
|
||||
381
packages/@uppy/image-generator/src/index.tsx
Normal file
381
packages/@uppy/image-generator/src/index.tsx
Normal file
|
|
@ -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<OptionsWithRestructuredFields>
|
||||
}
|
||||
|
||||
interface PluginState extends Record<string, unknown> {
|
||||
prompt: string
|
||||
results: AssemblyResult[]
|
||||
checkedResultIds: Set<AssemblyResult['id']>
|
||||
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<ImageGeneratorOptions, M, B, PluginState> {
|
||||
private loadingInterval: ReturnType<typeof setInterval> | null = null
|
||||
private rateLimitedQueue: RateLimitedQueue
|
||||
private client: Client<M, B>
|
||||
private assembly: Assembly | null = null
|
||||
icon: () => h.JSX.Element
|
||||
|
||||
constructor(uppy: Uppy<M, B>, 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 = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>ai-image-generator</title>
|
||||
<defs>
|
||||
<circle id="uppyImageGeneratorCircle" cx="12" cy="12" r="12" />
|
||||
</defs>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<mask id="uppyImageGeneratorMask" fill="#fff">
|
||||
<use xlinkHref="#uppyImageGeneratorCircle" />
|
||||
</mask>
|
||||
<use xlinkHref="#uppyImageGeneratorCircle" fill="#004b9d" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m21.98 15.453 2.793.254a.295.295 0 0 1 0 .586l-2.794.254a6 6 0 0 0-5.432 5.432l-.254 2.794a.295.295 0 0 1-.586 0l-.254-2.794a6 6 0 0 0-5.432-5.432l-2.794-.254a.295.295 0 0 1 0-.586l2.794-.254a6 6 0 0 0 5.432-5.432l.254-2.794a.295.295 0 0 1 .586 0l.254 2.794a6 6 0 0 0 5.432 5.432"
|
||||
mask="url(#uppyImageGeneratorMask)"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m10.74 7.75 1.434.13a.121.121 0 0 1 0 .24l-1.433.13a2.75 2.75 0 0 0-2.49 2.49l-.13 1.434a.121.121 0 0 1-.242 0l-.13-1.433a2.75 2.75 0 0 0-2.49-2.49l-1.433-.13a.121.121 0 0 1 0-.242l1.433-.13a2.75 2.75 0 0 0 2.49-2.49l.13-1.433a.121.121 0 0 1 .242 0l.13 1.433a2.75 2.75 0 0 0 2.49 2.49"
|
||||
mask="url(#uppyImageGeneratorMask)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
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<void>()
|
||||
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<MinimalRequiredUppyFile<M, B>>[] = 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 (
|
||||
<SearchView
|
||||
value={prompt}
|
||||
onChange={(prompt) => this.setPluginState({ prompt })}
|
||||
onSubmit={this.generate}
|
||||
inputLabel={i18n('generateImagePlaceholder')}
|
||||
loading={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<span className="uppy-ImageGenerator-generating">
|
||||
{currentLoadingMessage}
|
||||
</span>
|
||||
) : (
|
||||
i18n('generateImage')
|
||||
)}
|
||||
</SearchView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="uppy-ProviderBrowser uppy-ProviderBrowser-viewType--grid">
|
||||
<FilterInput
|
||||
value={prompt}
|
||||
onChange={(prompt) => this.setPluginState({ prompt })}
|
||||
onSubmit={this.generate}
|
||||
inputLabel={i18n('search')}
|
||||
i18n={i18n}
|
||||
/>
|
||||
|
||||
{loading ? (
|
||||
<div className="uppy-Provider-loading uppy-ImageGenerator-generating--darker">
|
||||
{currentLoadingMessage}
|
||||
</div>
|
||||
) : results.length > 0 ? (
|
||||
<div className="uppy-ProviderBrowser-body">
|
||||
<ul className="uppy-ProviderBrowser-list" tabIndex={-1}>
|
||||
{results.map((result) => (
|
||||
<li
|
||||
key={result.id}
|
||||
className={`uppy-ProviderBrowserItem ${checkedResultIds.has(result.id) ? 'uppy-ProviderBrowserItem--is-checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="uppy-u-reset uppy-ProviderBrowserItem-checkbox uppy-ProviderBrowserItem-checkbox--grid"
|
||||
onChange={(e) => this.onCheckboxChange(result, e)}
|
||||
name="listitem"
|
||||
id={result.id}
|
||||
checked={checkedResultIds.has(result.id)}
|
||||
data-uppy-super-focusable
|
||||
/>
|
||||
<label
|
||||
htmlFor={result.id}
|
||||
aria-label={prompt}
|
||||
className="uppy-u-reset uppy-ProviderBrowserItem-inner"
|
||||
>
|
||||
<img
|
||||
src={result.url!}
|
||||
alt={prompt}
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="uppy-Provider-empty">{i18n('noSearchResults')}</div>
|
||||
)}
|
||||
|
||||
{checkedResultIds.size > 0 && (
|
||||
<div className="uppy-ProviderBrowser-footer">
|
||||
<div className="uppy-ProviderBrowser-footer-buttons">
|
||||
<button
|
||||
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary"
|
||||
onClick={this.donePicking}
|
||||
type="button"
|
||||
>
|
||||
{i18n('selectX', {
|
||||
smart_count: checkedResultIds.size,
|
||||
})}
|
||||
</button>
|
||||
<button
|
||||
className="uppy-u-reset uppy-c-btn uppy-c-btn-link"
|
||||
onClick={this.cancelSelection}
|
||||
type="button"
|
||||
>
|
||||
{i18n('cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
12
packages/@uppy/image-generator/src/locale.ts
Normal file
12
packages/@uppy/image-generator/src/locale.ts
Normal file
|
|
@ -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...',
|
||||
},
|
||||
}
|
||||
29
packages/@uppy/image-generator/src/style.scss
Normal file
29
packages/@uppy/image-generator/src/style.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
17
packages/@uppy/image-generator/tsconfig.build.json
Normal file
17
packages/@uppy/image-generator/tsconfig.build.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
packages/@uppy/image-generator/tsconfig.json
Normal file
22
packages/@uppy/image-generator/tsconfig.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
8
packages/@uppy/image-generator/turbo.json
Normal file
8
packages/@uppy/image-generator/turbo.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build", "@uppy/core#build", "@uppy/transloadit#build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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:',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
68
packages/@uppy/provider-views/src/FilterInput.tsx
Normal file
68
packages/@uppy/provider-views/src/FilterInput.tsx
Normal file
|
|
@ -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 (
|
||||
<section className="uppy-ProviderBrowser-searchFilter">
|
||||
<input
|
||||
className="uppy-u-reset uppy-ProviderBrowser-searchFilterInput"
|
||||
type="search"
|
||||
aria-label={inputLabel}
|
||||
placeholder={inputLabel}
|
||||
value={value}
|
||||
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
form={formId}
|
||||
data-uppy-super-focusable
|
||||
/>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="uppy-c-icon uppy-ProviderBrowser-searchFilterIcon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path d="M8.638 7.99l3.172 3.172a.492.492 0 1 1-.697.697L7.91 8.656a4.977 4.977 0 0 1-2.983.983C2.206 9.639 0 7.481 0 4.819 0 2.158 2.206 0 4.927 0c2.721 0 4.927 2.158 4.927 4.82a4.74 4.74 0 0 1-1.216 3.17zm-3.71.685c2.176 0 3.94-1.726 3.94-3.856 0-2.129-1.764-3.855-3.94-3.855C2.75.964.984 2.69.984 4.819c0 2.13 1.765 3.856 3.942 3.856z" />
|
||||
</svg>
|
||||
{value && (
|
||||
<button
|
||||
className="uppy-u-reset uppy-ProviderBrowser-searchFilterReset"
|
||||
type="button"
|
||||
aria-label={i18n('resetFilter')}
|
||||
onClick={() => onChange('')}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="uppy-c-icon"
|
||||
viewBox="0 0 19 19"
|
||||
>
|
||||
<path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterInput
|
||||
|
|
@ -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<M extends Meta, B extends Body> {
|
|||
i18n={i18n}
|
||||
/>
|
||||
{opts.showFilter && (
|
||||
<SearchInput
|
||||
searchString={searchString}
|
||||
setSearchString={(s: string) => this.onSearchInput(s)}
|
||||
submitSearchString={() => {}}
|
||||
<FilterInput
|
||||
value={searchString}
|
||||
onChange={(s: string) => this.onSearchInput(s)}
|
||||
onSubmit={() => {}}
|
||||
inputLabel={i18n('filter')}
|
||||
clearSearchLabel={i18n('resetFilter')}
|
||||
wrapperClassName="uppy-ProviderBrowser-searchFilter"
|
||||
inputClassName="uppy-ProviderBrowser-searchFilterInput"
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <form>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 (
|
||||
<section className={wrapperClassName}>
|
||||
<input
|
||||
className={`uppy-u-reset ${inputClassName}`}
|
||||
type="search"
|
||||
aria-label={inputLabel}
|
||||
placeholder={inputLabel}
|
||||
value={searchString}
|
||||
onInput={onInput}
|
||||
form={form.id}
|
||||
data-uppy-super-focusable
|
||||
/>
|
||||
{!showButton && (
|
||||
// 🔍
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="uppy-c-icon uppy-ProviderBrowser-searchFilterIcon"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path d="M8.638 7.99l3.172 3.172a.492.492 0 1 1-.697.697L7.91 8.656a4.977 4.977 0 0 1-2.983.983C2.206 9.639 0 7.481 0 4.819 0 2.158 2.206 0 4.927 0c2.721 0 4.927 2.158 4.927 4.82a4.74 4.74 0 0 1-1.216 3.17zm-3.71.685c2.176 0 3.94-1.726 3.94-3.856 0-2.129-1.764-3.855-3.94-3.855C2.75.964.984 2.69.984 4.819c0 2.13 1.765 3.856 3.942 3.856z" />
|
||||
</svg>
|
||||
)}
|
||||
{!showButton && searchString && (
|
||||
// ❌
|
||||
<button
|
||||
className="uppy-u-reset uppy-ProviderBrowser-searchFilterReset"
|
||||
type="button"
|
||||
aria-label={clearSearchLabel}
|
||||
title={clearSearchLabel}
|
||||
onClick={() => setSearchString('')}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className="uppy-c-icon"
|
||||
viewBox="0 0 19 19"
|
||||
>
|
||||
<path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{showButton && (
|
||||
<button
|
||||
className={`uppy-u-reset uppy-c-btn uppy-c-btn-primary ${buttonCSSClassName}`}
|
||||
type="submit"
|
||||
form={form.id}
|
||||
>
|
||||
{buttonLabel}
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
|
|
@ -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<M extends Meta, B extends Body> {
|
|||
|
||||
if (isInputMode) {
|
||||
return (
|
||||
<SearchInput
|
||||
searchString={searchString}
|
||||
setSearchString={this.setSearchString}
|
||||
submitSearchString={this.search}
|
||||
<SearchView
|
||||
value={searchString}
|
||||
onChange={this.setSearchString}
|
||||
onSubmit={this.search}
|
||||
inputLabel={i18n('enterTextToSearch')}
|
||||
buttonLabel={i18n('searchImages')}
|
||||
wrapperClassName="uppy-SearchProvider"
|
||||
inputClassName="uppy-c-textInput uppy-SearchProvider-input"
|
||||
showButton
|
||||
buttonCSSClassName="uppy-SearchProvider-searchButton"
|
||||
/>
|
||||
>
|
||||
{i18n('searchImages')}
|
||||
</SearchView>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -300,14 +298,12 @@ export default class SearchProviderView<M extends Meta, B extends Body> {
|
|||
)}
|
||||
>
|
||||
{opts.showFilter && (
|
||||
<SearchInput
|
||||
searchString={searchString}
|
||||
setSearchString={this.setSearchString}
|
||||
submitSearchString={this.search}
|
||||
<FilterInput
|
||||
value={searchString}
|
||||
onChange={this.setSearchString}
|
||||
onSubmit={this.search}
|
||||
inputLabel={i18n('search')}
|
||||
clearSearchLabel={i18n('resetSearch')}
|
||||
wrapperClassName="uppy-ProviderBrowser-searchFilter"
|
||||
inputClassName="uppy-ProviderBrowser-searchFilterInput"
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
53
packages/@uppy/provider-views/src/SearchView.tsx
Normal file
53
packages/@uppy/provider-views/src/SearchView.tsx
Normal file
|
|
@ -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 (
|
||||
<section className="uppy-SearchProvider">
|
||||
<input
|
||||
className="uppy-u-reset uppy-c-textInput uppy-SearchProvider-input"
|
||||
type="search"
|
||||
aria-label={inputLabel}
|
||||
placeholder={inputLabel}
|
||||
value={value}
|
||||
onInput={(e) => onChange((e.target as HTMLInputElement).value)}
|
||||
form={formId}
|
||||
disabled={loading}
|
||||
data-uppy-super-focusable
|
||||
/>
|
||||
<button
|
||||
disabled={loading}
|
||||
className="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-SearchProvider-searchButton"
|
||||
type="submit"
|
||||
form={formId}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchView
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
42
packages/@uppy/provider-views/src/useSearchForm.ts
Normal file
42
packages/@uppy/provider-views/src/useSearchForm.ts
Normal file
|
|
@ -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 <form>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 }
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SearchInput
|
||||
searchString={webdavUrl}
|
||||
setSearchString={setWebdavUrl}
|
||||
submitSearchString={onSubmit}
|
||||
<SearchView
|
||||
value={webdavUrl}
|
||||
onChange={setWebdavUrl}
|
||||
onSubmit={onSubmit}
|
||||
inputLabel={i18n('pluginWebdavInputLabel')}
|
||||
buttonLabel={i18n('authenticate')}
|
||||
wrapperClassName="uppy-SearchProvider"
|
||||
inputClassName="uppy-c-textInput uppy-SearchProvider-input"
|
||||
showButton
|
||||
buttonCSSClassName="uppy-SearchProvider-searchButton"
|
||||
/>
|
||||
>
|
||||
{i18n('authenticate')}
|
||||
</SearchView>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
83
yarn.lock
83
yarn.lock
|
|
@ -4975,18 +4975,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/eslint-utils@npm:^4.2.0":
|
||||
version: 4.7.0
|
||||
resolution: "@eslint-community/eslint-utils@npm:4.7.0"
|
||||
dependencies:
|
||||
eslint-visitor-keys: "npm:^3.4.3"
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
||||
checksum: 10/43ed5d391526d9f5bbe452aef336389a473026fca92057cf97c576db11401ce9bcf8ef0bf72625bbaf6207ed8ba6bf0dcf4d7e809c24f08faa68a28533c491a7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/eslint-utils@npm:^4.4.0":
|
||||
"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "@eslint-community/eslint-utils@npm:4.4.0"
|
||||
dependencies:
|
||||
|
|
@ -10983,6 +10972,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"
|
||||
|
|
@ -11206,7 +11213,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:
|
||||
|
|
@ -12619,12 +12626,12 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.12
|
||||
resolution: "brace-expansion@npm:1.1.12"
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
dependencies:
|
||||
balanced-match: "npm:^1.0.0"
|
||||
concat-map: "npm:0.0.1"
|
||||
checksum: 10/12cb6d6310629e3048cadb003e1aca4d8c9bb5c67c3c321bafdd7e7a50155de081f78ea3e0ed92ecc75a9015e784f301efc8132383132f4f7904ad1ac529c562
|
||||
checksum: 10/faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -13283,7 +13290,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"combined-stream@npm:^1.0.8":
|
||||
"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "combined-stream@npm:1.0.8"
|
||||
dependencies:
|
||||
|
|
@ -14264,9 +14271,9 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"devalue@npm:^5.1.0":
|
||||
version: 5.3.2
|
||||
resolution: "devalue@npm:5.3.2"
|
||||
checksum: 10/2e15e9ed6844e86f1d086088c0f51e447e2300ee385ace02acca4691563ca13e5d0f5445603afc52653609078039c9dad69bd8204dadfac0919fe1ef65f08a88
|
||||
version: 5.1.1
|
||||
resolution: "devalue@npm:5.1.1"
|
||||
checksum: 10/ff36fe61af61636419eb16692d2fe43d793cdfb17f868bb3560c5485e4c25bc38d472304e61efdec6e806a3c4b450c46247decf59968b4a05ddc7714ea64f885
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -15296,11 +15303,11 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"esquery@npm:^1.4.2":
|
||||
version: 1.6.0
|
||||
resolution: "esquery@npm:1.6.0"
|
||||
version: 1.5.0
|
||||
resolution: "esquery@npm:1.5.0"
|
||||
dependencies:
|
||||
estraverse: "npm:^5.1.0"
|
||||
checksum: 10/c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a
|
||||
checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -16322,16 +16329,13 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"form-data@npm:^2.5.0":
|
||||
version: 2.5.5
|
||||
resolution: "form-data@npm:2.5.5"
|
||||
version: 2.5.1
|
||||
resolution: "form-data@npm:2.5.1"
|
||||
dependencies:
|
||||
asynckit: "npm:^0.4.0"
|
||||
combined-stream: "npm:^1.0.8"
|
||||
es-set-tostringtag: "npm:^2.1.0"
|
||||
hasown: "npm:^2.0.2"
|
||||
mime-types: "npm:^2.1.35"
|
||||
safe-buffer: "npm:^5.2.1"
|
||||
checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d
|
||||
combined-stream: "npm:^1.0.6"
|
||||
mime-types: "npm:^2.1.12"
|
||||
checksum: 10/2e2e5e927979ba3623f9b4c4bcc939275fae3f2dea9dafc8db3ca656a3d75476605de2c80f0e6f1487987398e056f0b4c738972d6e1edd83392d5686d0952eed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -17480,17 +17484,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"import-fresh@npm:^3.2.1":
|
||||
version: 3.3.1
|
||||
resolution: "import-fresh@npm:3.3.1"
|
||||
dependencies:
|
||||
parent-module: "npm:^1.0.0"
|
||||
resolve-from: "npm:^4.0.0"
|
||||
checksum: 10/a06b19461b4879cc654d46f8a6244eb55eb053437afd4cbb6613cad6be203811849ed3e4ea038783092879487299fda24af932b86bdfff67c9055ba3612b8c87
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"import-fresh@npm:^3.3.0":
|
||||
"import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "import-fresh@npm:3.3.0"
|
||||
dependencies:
|
||||
|
|
@ -19627,7 +19621,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.26, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
|
||||
"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.26, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
|
||||
version: 2.1.35
|
||||
resolution: "mime-types@npm:2.1.35"
|
||||
dependencies:
|
||||
|
|
@ -23366,7 +23360,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0":
|
||||
"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451
|
||||
|
|
@ -25698,6 +25692,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:*"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue