Bug fix: make dismiss during export not fire success toast (#3882)
* Bug fix: make dismiss during export not fire success toast * Fix export fail test, since this failure errors early now * Remove throttling from send side * Move toast.loading out to when first engine command is sent, so it is shown immediately * Use shared, named constants for toast messages * Hook up a couple other error toasts to the `pendingExport.toastId`
This commit is contained in:
@ -346,10 +346,7 @@ const sketch001 = startSketchAt([-0, -0])
|
|||||||
// Find the toast.
|
// Find the toast.
|
||||||
// Look out for the toast message
|
// Look out for the toast message
|
||||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||||
await expect(exportingToastMessage).toBeVisible()
|
|
||||||
|
|
||||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||||
await expect(errorToastMessage).toBeVisible()
|
|
||||||
|
|
||||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||||
await expect(engineErrorToastMessage).toBeVisible()
|
await expect(engineErrorToastMessage).toBeVisible()
|
||||||
|
@ -415,20 +415,9 @@ export const ModelingMachineProvider = ({
|
|||||||
selection: { type: 'default_scene' },
|
selection: { type: 'default_scene' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artificially delay the export in playwright tests
|
|
||||||
toast
|
|
||||||
.promise(
|
|
||||||
exportFromEngine({
|
exportFromEngine({
|
||||||
format: format,
|
format: format,
|
||||||
}),
|
}).catch(reportRejection)
|
||||||
|
|
||||||
{
|
|
||||||
loading: 'Starting print...',
|
|
||||||
success: 'Started print successfully',
|
|
||||||
error: 'Error while starting print',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch(reportRejection)
|
|
||||||
},
|
},
|
||||||
'Engine export': ({ event }) => {
|
'Engine export': ({ event }) => {
|
||||||
if (event.type !== 'Export') return
|
if (event.type !== 'Export') return
|
||||||
@ -482,18 +471,9 @@ export const ModelingMachineProvider = ({
|
|||||||
format.selection = { type: 'default_scene' }
|
format.selection = { type: 'default_scene' }
|
||||||
}
|
}
|
||||||
|
|
||||||
toast
|
|
||||||
.promise(
|
|
||||||
exportFromEngine({
|
exportFromEngine({
|
||||||
format: format as Models['OutputFormat_type'],
|
format: format as Models['OutputFormat_type'],
|
||||||
}),
|
}).catch(reportRejection)
|
||||||
{
|
|
||||||
loading: 'Exporting...',
|
|
||||||
success: 'Exported successfully',
|
|
||||||
error: 'Error while exporting',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch(reportRejection)
|
|
||||||
},
|
},
|
||||||
'Submit to Text-to-CAD API': ({ event }) => {
|
'Submit to Text-to-CAD API': ({ event }) => {
|
||||||
if (event.type !== 'Text-to-CAD') return
|
if (event.type !== 'Text-to-CAD') return
|
||||||
@ -591,7 +571,9 @@ export const ModelingMachineProvider = ({
|
|||||||
else if (kclManager.ast.body.length === 0)
|
else if (kclManager.ast.body.length === 0)
|
||||||
errorMessage += 'due to Empty Scene'
|
errorMessage += 'due to Empty Scene'
|
||||||
console.error(errorMessage)
|
console.error(errorMessage)
|
||||||
toast.error(errorMessage)
|
toast.error(errorMessage, {
|
||||||
|
id: kclManager.engineCommandManager.pendingExport?.toastId,
|
||||||
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,11 @@ import { useModelingContext } from 'hooks/useModelingContext'
|
|||||||
import { exportMake } from 'lib/exportMake'
|
import { exportMake } from 'lib/exportMake'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
|
||||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
|
import {
|
||||||
|
EXECUTE_AST_INTERRUPT_ERROR_MESSAGE,
|
||||||
|
EXPORT_TOAST_MESSAGES,
|
||||||
|
MAKE_TOAST_MESSAGES,
|
||||||
|
} from 'lib/constants'
|
||||||
import { KclManager } from 'lang/KclSingleton'
|
import { KclManager } from 'lang/KclSingleton'
|
||||||
import { reportRejection } from 'lib/trap'
|
import { reportRejection } from 'lib/trap'
|
||||||
|
|
||||||
@ -959,7 +963,9 @@ class EngineConnection extends EventTarget {
|
|||||||
) {
|
) {
|
||||||
// Reject the promise with the error.
|
// Reject the promise with the error.
|
||||||
this.engineCommandManager.pendingExport.reject(errorsString)
|
this.engineCommandManager.pendingExport.reject(errorsString)
|
||||||
toast.error(errorsString)
|
toast.error(errorsString, {
|
||||||
|
id: this.engineCommandManager.pendingExport.toastId,
|
||||||
|
})
|
||||||
this.engineCommandManager.pendingExport = undefined
|
this.engineCommandManager.pendingExport = undefined
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1327,8 +1333,13 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
defaultPlanes: DefaultPlanes | null = null
|
defaultPlanes: DefaultPlanes | null = null
|
||||||
commandLogs: CommandLog[] = []
|
commandLogs: CommandLog[] = []
|
||||||
pendingExport?: {
|
pendingExport?: {
|
||||||
|
/** The id of the shared loading/success/error toast for export */
|
||||||
|
toastId: string
|
||||||
|
/** An on-success callback */
|
||||||
resolve: (a: null) => void
|
resolve: (a: null) => void
|
||||||
|
/** An on-error callback */
|
||||||
reject: (reason: string) => void
|
reject: (reason: string) => void
|
||||||
|
/** The engine command uuid */
|
||||||
commandId: string
|
commandId: string
|
||||||
}
|
}
|
||||||
settings: SettingsViaQueryString
|
settings: SettingsViaQueryString
|
||||||
@ -1590,7 +1601,7 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
// because in all other cases we send JSON strings. But in the case of
|
// because in all other cases we send JSON strings. But in the case of
|
||||||
// export we send a binary blob.
|
// export we send a binary blob.
|
||||||
// Pass this to our export function.
|
// Pass this to our export function.
|
||||||
if (this.exportIntent === null) {
|
if (this.exportIntent === null || this.pendingExport === undefined) {
|
||||||
toast.error(
|
toast.error(
|
||||||
'Export intent was not set, but export data was received'
|
'Export intent was not set, but export data was received'
|
||||||
)
|
)
|
||||||
@ -1602,19 +1613,22 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
|
|
||||||
switch (this.exportIntent) {
|
switch (this.exportIntent) {
|
||||||
case ExportIntent.Save: {
|
case ExportIntent.Save: {
|
||||||
exportSave(event.data).then(() => {
|
exportSave(event.data, this.pendingExport.toastId).then(() => {
|
||||||
this.pendingExport?.resolve(null)
|
this.pendingExport?.resolve(null)
|
||||||
}, this.pendingExport?.reject)
|
}, this.pendingExport?.reject)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case ExportIntent.Make: {
|
case ExportIntent.Make: {
|
||||||
exportMake(event.data).then((result) => {
|
exportMake(event.data, this.pendingExport.toastId).then(
|
||||||
|
(result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
this.pendingExport?.resolve(null)
|
this.pendingExport?.resolve(null)
|
||||||
} else {
|
} else {
|
||||||
this.pendingExport?.reject('Failed to make export')
|
this.pendingExport?.reject('Failed to make export')
|
||||||
}
|
}
|
||||||
}, this.pendingExport?.reject)
|
},
|
||||||
|
this.pendingExport?.reject
|
||||||
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1929,7 +1943,20 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
return Promise.resolve(null)
|
return Promise.resolve(null)
|
||||||
} else if (cmd.type === 'export') {
|
} else if (cmd.type === 'export') {
|
||||||
const promise = new Promise<null>((resolve, reject) => {
|
const promise = new Promise<null>((resolve, reject) => {
|
||||||
|
if (this.exportIntent === null) {
|
||||||
|
if (this.exportIntent === null) {
|
||||||
|
toast.error('Export intent was not set, but export is being sent')
|
||||||
|
console.error('Export intent was not set, but export is being sent')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toastId = toast.loading(
|
||||||
|
this.exportIntent === ExportIntent.Save
|
||||||
|
? EXPORT_TOAST_MESSAGES.START
|
||||||
|
: MAKE_TOAST_MESSAGES.START
|
||||||
|
)
|
||||||
this.pendingExport = {
|
this.pendingExport = {
|
||||||
|
toastId,
|
||||||
resolve: (passThrough) => {
|
resolve: (passThrough) => {
|
||||||
this.addCommandLog({
|
this.addCommandLog({
|
||||||
type: 'export-done',
|
type: 'export-done',
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
/// The method below uses the File System Access API when it's supported and
|
/// The method below uses the File System Access API when it's supported and
|
||||||
// else falls back to the classic approach. In both cases the function saves
|
// else falls back to the classic approach. In both cases the function saves
|
||||||
// the file, but in case of where the File System Access API is supported, the
|
// the file, but in case of where the File System Access API is supported, the
|
||||||
|
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { EXPORT_TOAST_MESSAGES } from './constants'
|
||||||
|
|
||||||
// user will get a file save dialog where they can choose where the file should be saved.
|
// user will get a file save dialog where they can choose where the file should be saved.
|
||||||
export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
|
export const browserSaveFile = async (
|
||||||
|
blob: Blob,
|
||||||
|
suggestedName: string,
|
||||||
|
toastId: string
|
||||||
|
) => {
|
||||||
// Feature detection. The API needs to be supported
|
// Feature detection. The API needs to be supported
|
||||||
// and the app not run in an iframe.
|
// and the app not run in an iframe.
|
||||||
const supportsFileSystemAccess =
|
const supportsFileSystemAccess =
|
||||||
@ -29,11 +37,15 @@ export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
|
|||||||
const writable = await handle.createWritable()
|
const writable = await handle.createWritable()
|
||||||
await writable.write(blob)
|
await writable.write(blob)
|
||||||
await writable.close()
|
await writable.close()
|
||||||
|
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
|
||||||
return
|
return
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Fail silently if the user has simply canceled the dialog.
|
// Fail silently if the user has simply canceled the dialog.
|
||||||
if (err.name !== 'AbortError') {
|
if (err.name === 'AbortError') {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
} else {
|
||||||
console.error(err.name, err.message)
|
console.error(err.name, err.message)
|
||||||
|
toast.error(EXPORT_TOAST_MESSAGES.FAILED, { id: toastId })
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -54,4 +66,5 @@ export const browserSaveFile = async (blob: Blob, suggestedName: string) => {
|
|||||||
URL.revokeObjectURL(blobURL)
|
URL.revokeObjectURL(blobURL)
|
||||||
a.remove()
|
a.remove()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
|
||||||
}
|
}
|
||||||
|
@ -77,3 +77,21 @@ export const PLAYWRIGHT_KEY = 'playwright'
|
|||||||
* allows us to match if the execution of executeAst was interrupted */
|
* allows us to match if the execution of executeAst was interrupted */
|
||||||
export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE =
|
export const EXECUTE_AST_INTERRUPT_ERROR_MESSAGE =
|
||||||
'Force interrupt, executionIsStale, new AST requested'
|
'Force interrupt, executionIsStale, new AST requested'
|
||||||
|
|
||||||
|
/** The messages that appear for exporting toasts */
|
||||||
|
export const EXPORT_TOAST_MESSAGES = {
|
||||||
|
START: 'Exporting...',
|
||||||
|
SUCCESS: 'Exported successfully',
|
||||||
|
FAILED: 'Export failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The messages that appear for "make" command toasts */
|
||||||
|
export const MAKE_TOAST_MESSAGES = {
|
||||||
|
START: 'Starting print...',
|
||||||
|
NO_MACHINES: 'No machines available',
|
||||||
|
NO_MACHINE_API_IP: 'No machine api ip available',
|
||||||
|
NO_CURRENT_MACHINE: 'No current machine available',
|
||||||
|
NO_MACHINE_ID: 'No machine id available',
|
||||||
|
ERROR_STARTING_PRINT: 'Error while starting print',
|
||||||
|
SUCCESS: 'Started print successfully',
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { engineCommandManager } from 'lib/singletons'
|
import { engineCommandManager } from 'lib/singletons'
|
||||||
import { type Models } from '@kittycad/lib'
|
import { type Models } from '@kittycad/lib'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
|
||||||
|
|
||||||
// Isolating a function to call the engine to export the current scene.
|
// Isolating a function to call the engine to export the current scene.
|
||||||
// Because it has given us trouble in automated testing environments.
|
// Because it has given us trouble in automated testing environments.
|
||||||
@ -23,11 +22,5 @@ export async function exportFromEngine({
|
|||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// If we are in playwright slow down the export.
|
|
||||||
const inPlaywright = window.localStorage.getItem(IS_PLAYWRIGHT_KEY)
|
|
||||||
if (inPlaywright === 'true') {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
||||||
}
|
|
||||||
|
|
||||||
return exportPromise
|
return exportPromise
|
||||||
}
|
}
|
||||||
|
@ -3,33 +3,37 @@ import { machineManager } from './machineManager'
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { components } from './machine-api'
|
import { components } from './machine-api'
|
||||||
import ModelingAppFile from './modelingAppFile'
|
import ModelingAppFile from './modelingAppFile'
|
||||||
|
import { MAKE_TOAST_MESSAGES } from './constants'
|
||||||
|
|
||||||
// Make files locally from an export call.
|
// Make files locally from an export call.
|
||||||
export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
|
export async function exportMake(
|
||||||
|
data: ArrayBuffer,
|
||||||
|
toastId: string
|
||||||
|
): Promise<Response | null> {
|
||||||
if (machineManager.machineCount() === 0) {
|
if (machineManager.machineCount() === 0) {
|
||||||
console.error('No machines available')
|
console.error(MAKE_TOAST_MESSAGES.NO_MACHINES)
|
||||||
toast.error('No machines available')
|
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINES, { id: toastId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const machineApiIp = machineManager.machineApiIp
|
const machineApiIp = machineManager.machineApiIp
|
||||||
if (!machineApiIp) {
|
if (!machineApiIp) {
|
||||||
console.error('No machine api ip available')
|
console.error(MAKE_TOAST_MESSAGES.NO_MACHINE_API_IP)
|
||||||
toast.error('No machine api ip available')
|
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINE_API_IP, { id: toastId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentMachine = machineManager.currentMachine
|
const currentMachine = machineManager.currentMachine
|
||||||
if (!currentMachine) {
|
if (!currentMachine) {
|
||||||
console.error('No current machine available')
|
console.error(MAKE_TOAST_MESSAGES.NO_CURRENT_MACHINE)
|
||||||
toast.error('No current machine available')
|
toast.error(MAKE_TOAST_MESSAGES.NO_CURRENT_MACHINE, { id: toastId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let machineId = currentMachine?.id
|
let machineId = currentMachine?.id
|
||||||
if (!machineId) {
|
if (!machineId) {
|
||||||
console.error('No machine id available', currentMachine)
|
console.error(MAKE_TOAST_MESSAGES.NO_MACHINE_ID, currentMachine)
|
||||||
toast.error('No machine id available')
|
toast.error(MAKE_TOAST_MESSAGES.NO_MACHINE_ID, { id: toastId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,16 +62,22 @@ export async function exportMake(data: ArrayBuffer): Promise<Response | null> {
|
|||||||
console.log('response', response)
|
console.log('response', response)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Error exporting', response)
|
console.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, response)
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
toast.error('Error exporting: ' + response.statusText + ' ' + text)
|
toast.error(
|
||||||
|
'Error while starting print: ' + response.statusText + ' ' + text,
|
||||||
|
{
|
||||||
|
id: toastId,
|
||||||
|
}
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.success(MAKE_TOAST_MESSAGES.SUCCESS, { id: toastId })
|
||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error exporting', error)
|
console.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, error)
|
||||||
toast.error('Error exporting')
|
toast.error(MAKE_TOAST_MESSAGES.ERROR_STARTING_PRINT, { id: toastId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,10 @@ import { browserSaveFile } from './browserSaveFile'
|
|||||||
|
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import ModelingAppFile from './modelingAppFile'
|
import ModelingAppFile from './modelingAppFile'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { EXPORT_TOAST_MESSAGES } from './constants'
|
||||||
|
|
||||||
const save_ = async (file: ModelingAppFile) => {
|
const save_ = async (file: ModelingAppFile, toastId: string) => {
|
||||||
try {
|
try {
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
const extension = file.name.split('.').pop() || null
|
const extension = file.name.split('.').pop() || null
|
||||||
@ -20,6 +22,7 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
file.name,
|
file.name,
|
||||||
new Uint8Array(file.contents)
|
new Uint8Array(file.contents)
|
||||||
)
|
)
|
||||||
|
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,13 +39,17 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
|
|
||||||
// The user canceled the save.
|
// The user canceled the save.
|
||||||
// Return early.
|
// Return early.
|
||||||
if (filePathMeta.canceled) return
|
if (filePathMeta.canceled) {
|
||||||
|
toast.dismiss(toastId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Write the file.
|
// Write the file.
|
||||||
await window.electron.writeFile(
|
await window.electron.writeFile(
|
||||||
filePathMeta.filePath,
|
filePathMeta.filePath,
|
||||||
new Uint8Array(file.contents)
|
new Uint8Array(file.contents)
|
||||||
)
|
)
|
||||||
|
toast.success(EXPORT_TOAST_MESSAGES.SUCCESS, { id: toastId })
|
||||||
} else {
|
} else {
|
||||||
// Download the file to the user's computer.
|
// Download the file to the user's computer.
|
||||||
// Now we need to download the files to the user's downloads folder.
|
// Now we need to download the files to the user's downloads folder.
|
||||||
@ -51,16 +58,17 @@ const save_ = async (file: ModelingAppFile) => {
|
|||||||
// Create a new blob.
|
// Create a new blob.
|
||||||
const blob = new Blob([new Uint8Array(file.contents)])
|
const blob = new Blob([new Uint8Array(file.contents)])
|
||||||
// Save the file.
|
// Save the file.
|
||||||
await browserSaveFile(blob, file.name)
|
await browserSaveFile(blob, file.name, toastId)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: do something real with the error.
|
// TODO: do something real with the error.
|
||||||
console.error('export error', e)
|
console.error('export error', e)
|
||||||
|
toast.error(EXPORT_TOAST_MESSAGES.FAILED, { id: toastId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves files locally from an export call.
|
// Saves files locally from an export call.
|
||||||
export async function exportSave(data: ArrayBuffer) {
|
export async function exportSave(data: ArrayBuffer, toastId: string) {
|
||||||
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
// This converts the ArrayBuffer to a Rust equivalent Vec<u8>.
|
||||||
let uintArray = new Uint8Array(data)
|
let uintArray = new Uint8Array(data)
|
||||||
|
|
||||||
@ -72,9 +80,9 @@ export async function exportSave(data: ArrayBuffer) {
|
|||||||
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
zip.file(file.name, new Uint8Array(file.contents), { binary: true })
|
||||||
}
|
}
|
||||||
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
return zip.generateAsync({ type: 'array' }).then((contents) => {
|
||||||
return save_({ name: 'output.zip', contents })
|
return save_({ name: 'output.zip', contents }, toastId)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return save_(files[0])
|
return save_(files[0], toastId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user