Compare commits

...

10 Commits

Author SHA1 Message Date
Christian Byrne
0178003156 Merge branch 'main' into refactor/consolidate-image-upload 2026-05-04 13:46:18 -07:00
bymyself
f9899fccaa test: add Load3dUtils upload coverage to lift PR patch coverage 2026-05-04 10:59:59 -07:00
bymyself
027d21817d test: add uploadService coverage for endpoint forwarding and Blob conversion 2026-05-04 10:43:38 -07:00
bymyself
5e632ed274 chore: drop unused api import from Load3dUtils
Now that all upload logic goes through uploadMedia, the direct api
client is no longer used in this file.
2026-05-04 01:16:05 -07:00
bymyself
258af63618 refactor: use uploadMedia in useWidgetSelectActions
Pivot consolidation target: main extracted upload logic from
WidgetSelectDropdown.vue into useWidgetSelectActions composable
(PR #10966 VTL migration). Refactor that composable to use the
shared uploadMedia service, eliminating the remaining duplicate
upload path.
2026-05-04 01:15:58 -07:00
bymyself
e57b6a52aa fix: polish uploadService based on code review feedback
- M2: Blob MIME type now prefers source.type over default parameter
- M3: Removed double toast on failed uploads
- m1: Converted WidgetSelectDropdown.vue to Vue 3.5 reactive props
- m3: Created createMockResponse() helper in tests
- m4: Added test for empty batch edge case
- m5: Removed verbose JSDoc comments

Amp-Thread-ID: https://ampcode.com/threads/T-019c265d-f2f5-7028-95b4-5e031e447bd3
2026-05-04 01:12:53 -07:00
bymyself
142c372823 fix: address CodeRabbit review comments
- Remove data URL content from error messages (security)
- Replace 'as any' casts with proper type assertions in tests
- Use uploadMediaBatch in WidgetSelectDropdown for single store update
- Localize error strings with i18n (uploadFailed, tempUploadFailed)

Amp-Thread-ID: https://ampcode.com/threads/T-019c0daa-eb18-72eb-bc87-90f09afc3d3a
Co-authored-by: Amp <amp@ampcode.com>
2026-05-04 01:12:49 -07:00
bymyself
e1dca0bcb2 Fix type errors after rebase
- Add back api import to Load3dUtils (needed by fileExists and uploadThumbnail)
- Add null check for result.response in uploadTempImage
- Ensures type safety after rebase on main
2026-05-04 01:12:40 -07:00
bymyself
82935499d8 Address CodeRabbit review comments
- Import ImageRef from maskEditorDataStore instead of duplicating
- Replace 'any' with proper UploadApiResponse type
- Add validation for dataURL strings
- Fix test mocking: use vi.spyOn for global.fetch
- Fix uploadMediaBatch test to use distinct response mocks
- Add test for invalid dataURL rejection
- Fix uploadMultipleFiles to return array of successful paths
- Optimize file size test to avoid timeout
2026-05-04 01:12:40 -07:00
bymyself
9ef78eea0c Consolidate image upload implementations into shared service
Creates core uploadService to eliminate ~60-70 LOC duplication across multiple implementations.

Changes:
- Add src/platform/assets/services/uploadService.ts with uploadMedia() and uploadMediaBatch()
- Refactor Load3dUtils to use uploadService (eliminates 50+ LOC)
- Refactor WidgetSelectDropdown to use uploadService (eliminates 20+ LOC)
- Add comprehensive unit tests for uploadService
- Maintain backward compatibility for all existing APIs

Benefits:
- Single source of truth for upload logic
- Consistent error handling
- Type-safe interfaces
- Easier to test and maintain
2026-05-04 01:12:40 -07:00
7 changed files with 638 additions and 89 deletions

View File

@@ -658,8 +658,10 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
: '3d'
const uploadedPath = await Load3dUtils.uploadFile(file, subfolder)
sceneConfig.value.backgroundImage = uploadedPath
await load3d?.setBackgroundImage(uploadedPath)
if (uploadedPath) {
sceneConfig.value.backgroundImage = uploadedPath
await load3d?.setBackgroundImage(uploadedPath)
}
}
const handleExportModel = async (format: string) => {

View File

@@ -1,6 +1,20 @@
import { describe, expect, it } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { uploadMedia } from '@/platform/assets/services/uploadService'
import { useToastStore } from '@/platform/updates/common/toastStore'
vi.mock('@/platform/assets/services/uploadService', () => ({
uploadMedia: vi.fn()
}))
vi.mock('@/scripts/app', () => ({
app: {
getRandParam: () => '?rand=123'
}
}))
describe('Load3dUtils.mapSceneLightIntensityToHdri', () => {
it('maps scene slider low end to a small positive HDRI intensity', () => {
@@ -23,3 +37,158 @@ describe('Load3dUtils.mapSceneLightIntensityToHdri', () => {
expect(Load3dUtils.mapSceneLightIntensityToHdri(3, 5, 5)).toBe(0.25)
})
})
describe('Load3dUtils.uploadFile', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})
it('returns the uploaded path on success', async () => {
vi.mocked(uploadMedia).mockResolvedValue({
success: true,
path: 'subfolder/file.png',
name: 'file.png',
subfolder: 'subfolder',
response: { name: 'file.png', subfolder: 'subfolder' }
})
const file = new File(['x'], 'file.png')
const result = await Load3dUtils.uploadFile(file, 'subfolder')
expect(result).toBe('subfolder/file.png')
expect(uploadMedia).toHaveBeenCalledWith(
{ source: file },
{ subfolder: 'subfolder', maxSizeMB: Load3dUtils.MAX_UPLOAD_SIZE_MB }
)
})
it('shows file-too-large toast when size exceeds maximum', async () => {
vi.mocked(uploadMedia).mockResolvedValue({
success: false,
path: '',
name: '',
subfolder: '',
error: 'File size 200MB exceeds maximum 100MB',
response: null
})
const file = new File(['x'], 'big.png')
Object.defineProperty(file, 'size', {
value: 200 * 1024 * 1024,
writable: false
})
const result = await Load3dUtils.uploadFile(file, '3d')
expect(result).toBeUndefined()
const toastStore = useToastStore()
expect(toastStore.addAlert).toHaveBeenCalledOnce()
})
it('shows generic alert toast on other upload failures', async () => {
vi.mocked(uploadMedia).mockResolvedValue({
success: false,
path: '',
name: '',
subfolder: '',
error: 'Network error',
response: null
})
const result = await Load3dUtils.uploadFile(
new File(['x'], 'fail.png'),
'3d'
)
expect(result).toBeUndefined()
const toastStore = useToastStore()
expect(toastStore.addAlert).toHaveBeenCalledWith('Network error')
})
})
describe('Load3dUtils.uploadTempImage', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})
it('returns the upload response on success', async () => {
const response = { name: 'thumb_123.png', subfolder: 'threed' }
vi.mocked(uploadMedia).mockResolvedValue({
success: true,
path: 'threed/thumb_123.png',
name: 'thumb_123.png',
subfolder: 'threed',
response
})
const result = await Load3dUtils.uploadTempImage(
'data:image/png;base64,abc',
'thumb'
)
expect(result).toEqual(response)
expect(uploadMedia).toHaveBeenCalledWith(
expect.objectContaining({ source: 'data:image/png;base64,abc' }),
{ subfolder: 'threed', type: 'temp' }
)
})
it('throws and shows alert on failure', async () => {
vi.mocked(uploadMedia).mockResolvedValue({
success: false,
path: '',
name: '',
subfolder: '',
error: 'boom',
response: null
})
await expect(
Load3dUtils.uploadTempImage('data:image/png;base64,abc', 'thumb')
).rejects.toThrow()
const toastStore = useToastStore()
expect(toastStore.addAlert).toHaveBeenCalledOnce()
})
})
describe('Load3dUtils.uploadMultipleFiles', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
})
it('filters out failed uploads', async () => {
vi.mocked(uploadMedia)
.mockResolvedValueOnce({
success: true,
path: '3d/a.png',
name: 'a.png',
subfolder: '3d',
response: { name: 'a.png' }
})
.mockResolvedValueOnce({
success: false,
path: '',
name: '',
subfolder: '',
error: 'failed',
response: null
})
const files = [new File(['1'], 'a.png'), new File(['2'], 'b.png')]
const fileList = {
length: files.length,
item: (i: number) => files[i] ?? null,
[Symbol.iterator]: function* () {
for (const f of files) yield f
}
} as unknown as FileList
const results = await Load3dUtils.uploadMultipleFiles(fileList)
expect(results).toEqual(['3d/a.png'])
})
})

View File

@@ -1,6 +1,6 @@
import { t } from '@/i18n'
import { uploadMedia } from '@/platform/assets/services/uploadService'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
class Load3dUtils {
@@ -9,84 +9,54 @@ class Load3dUtils {
prefix: string,
fileType: string = 'png'
) {
const blob = await fetch(imageData).then((r) => r.blob())
const name = `${prefix}_${Date.now()}.${fileType}`
const file = new File([blob], name, {
type: fileType === 'mp4' ? 'video/mp4' : 'image/png'
})
const filename = `${prefix}_${Date.now()}.${fileType}`
const result = await uploadMedia(
{ source: imageData, filename },
{ subfolder: 'threed', type: 'temp' }
)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'threed')
body.append('type', 'temp')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading temp file: ${resp.status} - ${resp.statusText}`
if (!result.success || !result.response) {
const err = t('toastMessages.tempUploadFailed', {
error: result.error || ''
})
useToastStore().addAlert(err)
throw new Error(err)
}
return await resp.json()
return result.response
}
static readonly MAX_UPLOAD_SIZE_MB = 100
static async uploadFile(file: File, subfolder: string) {
let uploadPath
const result = await uploadMedia(
{ source: file },
{ subfolder, maxSizeMB: this.MAX_UPLOAD_SIZE_MB }
)
const fileSizeMB = file.size / 1024 / 1024
if (fileSizeMB > this.MAX_UPLOAD_SIZE_MB) {
const message = t('toastMessages.fileTooLarge', {
size: fileSizeMB.toFixed(1),
maxSize: this.MAX_UPLOAD_SIZE_MB
})
console.warn(
'[Load3D] uploadFile: file too large',
fileSizeMB.toFixed(2),
'MB'
)
useToastStore().addAlert(message)
if (!result.success) {
if (result.error?.includes('exceeds maximum')) {
const fileSizeMB = file.size / 1024 / 1024
const message = t('toastMessages.fileTooLarge', {
size: fileSizeMB.toFixed(1),
maxSize: this.MAX_UPLOAD_SIZE_MB
})
console.warn(
'[Load3D] uploadFile: file too large',
fileSizeMB.toFixed(2),
'MB'
)
useToastStore().addAlert(message)
} else {
console.error('[Load3D] uploadFile: exception', result.error)
useToastStore().addAlert(
result.error || t('toastMessages.fileUploadFailed')
)
}
return undefined
}
try {
const body = new FormData()
body.append('image', file)
body.append('subfolder', subfolder)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status === 200) {
const data = await resp.json()
let path = data.name
if (data.subfolder) {
path = data.subfolder + '/' + path
}
uploadPath = path
} else {
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
console.error('[Load3D] uploadFile: exception', error)
useToastStore().addAlert(
error instanceof Error
? error.message
: t('toastMessages.fileUploadFailed')
)
}
return uploadPath
return result.path
}
static getFilenameExtension(url: string): string | undefined {
@@ -124,12 +94,16 @@ class Load3dUtils {
return `/view?${params}`
}
static async uploadMultipleFiles(files: FileList, subfolder: string = '3d') {
static async uploadMultipleFiles(
files: FileList,
subfolder: string = '3d'
): Promise<string[]> {
const uploadPromises = Array.from(files).map((file) =>
this.uploadFile(file, subfolder)
)
await Promise.all(uploadPromises)
const results = await Promise.all(uploadPromises)
return results.filter((path): path is string => path !== undefined)
}
static mapSceneLightIntensityToHdri(

View File

@@ -2074,6 +2074,8 @@
"pleaseSelectNodesToGroup": "Please select the nodes (or other groups) to create a group for",
"emptyCanvas": "Empty canvas",
"fileUploadFailed": "File upload failed",
"uploadFailed": "Upload failed",
"tempUploadFailed": "Error uploading temp file: {error}",
"fileTooLarge": "File too large ({size} MB). Maximum supported size is {maxSize} MB",
"unableToGetModelFilePath": "Unable to get model file path",
"couldNotDetermineFileType": "Could not determine file type",

View File

@@ -0,0 +1,253 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { api } from '@/scripts/api'
import { uploadMedia, uploadMediaBatch } from './uploadService'
vi.mock('@/scripts/api', () => ({
api: {
fetchApi: vi.fn()
}
}))
function createMockResponse(
status: number,
data?: { name: string; subfolder?: string }
) {
return {
status,
statusText: status === 200 ? 'OK' : 'Error',
json: vi.fn().mockResolvedValue(data ?? {})
} as unknown as Response
}
describe('uploadService', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('uploadMedia', () => {
it('uploads File successfully', async () => {
const mockFile = new File(['content'], 'test.png', { type: 'image/png' })
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'test.png', subfolder: 'uploads' })
)
const result = await uploadMedia({ source: mockFile })
expect(result.success).toBe(true)
expect(result.path).toBe('uploads/test.png')
expect(result.name).toBe('test.png')
expect(result.subfolder).toBe('uploads')
})
it('uploads Blob successfully', async () => {
const mockBlob = new Blob(['content'], { type: 'image/png' })
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'upload-123.png', subfolder: '' })
)
const result = await uploadMedia({ source: mockBlob })
expect(result.success).toBe(true)
expect(result.path).toBe('upload-123.png')
})
it('uploads dataURL successfully', async () => {
const dataURL = 'data:image/png;base64,iVBORw0KGgo='
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue({
blob: () => Promise.resolve(new Blob(['content']))
} as Response)
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'upload-456.png', subfolder: '' })
)
try {
const result = await uploadMedia({ source: dataURL })
expect(result.success).toBe(true)
} finally {
fetchSpy.mockRestore()
}
})
it('rejects invalid dataURL', async () => {
const invalidURL = 'not-a-data-url'
const result = await uploadMedia({ source: invalidURL })
expect(result.success).toBe(false)
expect(result.error).toContain('Invalid data URL')
})
it('includes subfolder in FormData', async () => {
const mockFile = new File(['content'], 'test.png')
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'test.png' })
)
await uploadMedia(
{ source: mockFile },
{ subfolder: 'custom', type: 'input' }
)
const formData = vi.mocked(api.fetchApi).mock.calls[0][1]
?.body as FormData
expect(formData.get('subfolder')).toBe('custom')
expect(formData.get('type')).toBe('input')
})
it('validates file size', async () => {
// Create a file that reports as 200MB without actually allocating that much memory
const largeFile = new File(['content'], 'large.png')
Object.defineProperty(largeFile, 'size', {
value: 200 * 1024 * 1024,
writable: false
})
const result = await uploadMedia(
{ source: largeFile },
{ maxSizeMB: 100 }
)
expect(result.success).toBe(false)
expect(result.error).toContain('exceeds maximum')
})
it('handles upload errors', async () => {
const mockFile = new File(['content'], 'test.png')
vi.mocked(api.fetchApi).mockResolvedValue({
status: 500,
statusText: 'Internal Server Error'
} as unknown as Response)
const result = await uploadMedia({ source: mockFile })
expect(result.success).toBe(false)
expect(result.error).toBe('500 - Internal Server Error')
})
it('handles exceptions', async () => {
const mockFile = new File(['content'], 'test.png')
vi.mocked(api.fetchApi).mockRejectedValue(new Error('Network error'))
const result = await uploadMedia({ source: mockFile })
expect(result.success).toBe(false)
expect(result.error).toBe('Network error')
})
it('includes originalRef for mask uploads', async () => {
const mockFile = new File(['content'], 'mask.png')
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'mask.png' })
)
const originalRef = {
filename: 'original.png',
subfolder: 'images',
type: 'input'
}
await uploadMedia(
{ source: mockFile },
{ endpoint: '/upload/mask', originalRef }
)
const formData = vi.mocked(api.fetchApi).mock.calls[0][1]
?.body as FormData
expect(formData.get('original_ref')).toBe(JSON.stringify(originalRef))
})
it('forwards the configured endpoint to api.fetchApi', async () => {
const mockFile = new File(['content'], 'mask.png')
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'mask.png' })
)
await uploadMedia({ source: mockFile }, { endpoint: '/upload/mask' })
expect(api.fetchApi).toHaveBeenCalledWith(
'/upload/mask',
expect.objectContaining({ method: 'POST' })
)
})
it('defaults to /upload/image endpoint when not specified', async () => {
const mockFile = new File(['content'], 'test.png')
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'test.png' })
)
await uploadMedia({ source: mockFile })
expect(api.fetchApi).toHaveBeenCalledWith(
'/upload/image',
expect.objectContaining({ method: 'POST' })
)
})
it('converts Blob source to a File preserving mime type and fallback name', async () => {
const mockBlob = new Blob(['content'], { type: 'image/png' })
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'upload-123.png', subfolder: '' })
)
await uploadMedia({ source: mockBlob })
const formData = vi.mocked(api.fetchApi).mock.calls[0][1]
?.body as FormData
const uploadedFile = formData.get('image')
expect(uploadedFile).toBeInstanceOf(File)
expect((uploadedFile as File).type).toBe('image/png')
expect((uploadedFile as File).name).toMatch(/^upload-\d+\.png$/)
})
it('uses provided filename when converting a Blob', async () => {
const mockBlob = new Blob(['content'], { type: 'image/png' })
vi.mocked(api.fetchApi).mockResolvedValue(
createMockResponse(200, { name: 'custom.png', subfolder: '' })
)
await uploadMedia({ source: mockBlob, filename: 'custom.png' })
const formData = vi.mocked(api.fetchApi).mock.calls[0][1]
?.body as FormData
const uploadedFile = formData.get('image') as File
expect(uploadedFile.name).toBe('custom.png')
})
})
describe('uploadMediaBatch', () => {
it('returns empty array for empty input', async () => {
const results = await uploadMediaBatch([])
expect(results).toHaveLength(0)
expect(api.fetchApi).not.toHaveBeenCalled()
})
it('uploads multiple files', async () => {
const mockFiles = [
new File(['1'], 'file1.png'),
new File(['2'], 'file2.png')
]
vi.mocked(api.fetchApi)
.mockResolvedValueOnce(
createMockResponse(200, { name: 'file1.png', subfolder: '' })
)
.mockResolvedValueOnce(
createMockResponse(200, { name: 'file2.png', subfolder: '' })
)
const results = await uploadMediaBatch(
mockFiles.map((source) => ({ source }))
)
expect(results).toHaveLength(2)
expect(results[0].success).toBe(true)
expect(results[1].success).toBe(true)
})
})
})

View File

@@ -0,0 +1,155 @@
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import type { ImageRef } from '@/stores/maskEditorDataStore'
interface UploadInput {
source: File | Blob | string
filename?: string
}
interface UploadConfig {
subfolder?: string
type?: ResultItemType
endpoint?: '/upload/image' | '/upload/mask'
originalRef?: ImageRef
maxSizeMB?: number
}
interface UploadApiResponse {
name: string
subfolder?: string
type?: string
}
interface UploadResult {
success: boolean
path: string
name: string
subfolder: string
error?: string
response: UploadApiResponse | null
}
function isDataURL(str: string): boolean {
return typeof str === 'string' && str.startsWith('data:')
}
async function convertToFile(
input: UploadInput,
mimeType: string = 'image/png'
): Promise<File> {
const { source, filename } = input
if (source instanceof File) {
return source
}
if (source instanceof Blob) {
const name = filename || `upload-${Date.now()}.png`
return new File([source], name, { type: source.type || mimeType })
}
// dataURL string
if (!isDataURL(source)) {
throw new Error('Invalid data URL')
}
try {
const blob = await fetch(source).then((r) => r.blob())
const name = filename || `upload-${Date.now()}.png`
return new File([blob], name, { type: mimeType })
} catch (error) {
throw new Error(
`Failed to convert data URL to file: ${error instanceof Error ? error.message : String(error)}`
)
}
}
function validateFileSize(file: File, maxSizeMB?: number): string | null {
if (!maxSizeMB) return null
const fileSizeMB = file.size / 1024 / 1024
if (fileSizeMB > maxSizeMB) {
return `File size ${fileSizeMB.toFixed(1)}MB exceeds maximum ${maxSizeMB}MB`
}
return null
}
export async function uploadMedia(
input: UploadInput,
config: UploadConfig = {}
): Promise<UploadResult> {
const {
subfolder,
type,
endpoint = '/upload/image',
originalRef,
maxSizeMB
} = config
try {
const file = await convertToFile(input)
const sizeError = validateFileSize(file, maxSizeMB)
if (sizeError) {
return {
success: false,
path: '',
name: '',
subfolder: '',
error: sizeError,
response: null
}
}
const body = new FormData()
body.append('image', file)
if (subfolder) body.append('subfolder', subfolder)
if (type) body.append('type', type)
if (originalRef) body.append('original_ref', JSON.stringify(originalRef))
const resp = await api.fetchApi(endpoint, {
method: 'POST',
body
})
if (resp.status !== 200) {
return {
success: false,
path: '',
name: '',
subfolder: '',
error: `${resp.status} - ${resp.statusText}`,
response: null
}
}
const data: UploadApiResponse = await resp.json()
const path = data.subfolder ? `${data.subfolder}/${data.name}` : data.name
return {
success: true,
path,
name: data.name,
subfolder: data.subfolder || '',
response: data
}
} catch (error) {
return {
success: false,
path: '',
name: '',
subfolder: '',
error: error instanceof Error ? error.message : String(error),
response: null
}
}
}
export async function uploadMediaBatch(
inputs: UploadInput[],
config: UploadConfig = {}
): Promise<UploadResult[]> {
return Promise.all(inputs.map((input) => uploadMedia(input, config)))
}

View File

@@ -2,11 +2,12 @@ import { toValue } from 'vue'
import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { t } from '@/i18n'
import { uploadMedia } from '@/platform/assets/services/uploadService'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { FormDropdownItem } from '@/renderer/extensions/vueNodes/widgets/components/form/dropdown/types'
import type { ResultItemType } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useAssetsStore } from '@/stores/assetsStore'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
@@ -40,33 +41,26 @@ export function useWidgetSelectActions(options: UseWidgetSelectActionsOptions) {
isPasted: boolean = false,
formFields: Partial<{ type: ResultItemType }> = {}
) {
const body = new FormData()
body.append('image', file)
if (isPasted) body.append('subfolder', 'pasted')
else {
const subfolder = toValue(options.uploadSubfolder)
if (subfolder) body.append('subfolder', subfolder)
}
if (formFields.type) body.append('type', formFields.type)
const subfolder = isPasted
? 'pasted'
: (toValue(options.uploadSubfolder) ?? undefined)
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
const result = await uploadMedia(
{ source: file },
{ subfolder, type: formFields.type }
)
if (resp.status !== 200) {
toastStore.addAlert(resp.status + ' - ' + resp.statusText)
if (!result.success) {
toastStore.addAlert(result.error ?? t('toastMessages.uploadFailed'))
return null
}
const data = await resp.json()
if (formFields.type === 'input' || (!formFields.type && !isPasted)) {
const assetsStore = useAssetsStore()
await assetsStore.updateInputs()
}
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
return result.path
}
async function uploadFiles(files: File[]): Promise<string[]> {
@@ -85,7 +79,7 @@ export function useWidgetSelectActions(options: UseWidgetSelectActionsOptions) {
const uploadedPaths = await uploadFiles(files)
if (uploadedPaths.length === 0) {
toastStore.addAlert('File upload failed')
toastStore.addAlert(t('toastMessages.fileUploadFailed'))
return
}