feat: support import zip

This commit is contained in:
Joel
2026-01-23 15:22:56 +08:00
parent f8438704a6
commit 788deffa2b
2 changed files with 103 additions and 35 deletions

View File

@ -1,23 +1,84 @@
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { useToastContext } from '@/app/components/base/toast'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
import { DSLImportStatus } from '@/models/app'
import { importAppBundle } from '@/service/apps'
import { getRedirection } from '@/utils/app-redirection'
type DSLConfirmModalProps = {
file?: File
versions?: {
importedVersion: string
systemVersion: string
}
onCancel: () => void
onConfirm: () => void
onSuccess?: () => void
confirmDisabled?: boolean
}
const DSLConfirmModal = ({
file,
versions = { importedVersion: '', systemVersion: '' },
onCancel,
onConfirm,
onSuccess,
confirmDisabled = false,
}: DSLConfirmModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { push } = useRouter()
const { isCurrentWorkspaceEditor } = useAppContext()
const { handleCheckPluginDependencies } = usePluginDependencies()
const [isImporting, setIsImporting] = useState(false)
const isZipFile = !!file && file.name.toLowerCase().endsWith('.zip')
const handleConfirm = async () => {
if (confirmDisabled || isImporting)
return
if (!isZipFile) {
onConfirm()
return
}
if (!file)
return
setIsImporting(true)
try {
const response = await importAppBundle({ file })
const { status, app_id, app_mode } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
notify({
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }),
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (app_id)
await handleCheckPluginDependencies(app_id)
if (app_id)
getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push)
onSuccess?.()
onCancel()
}
else {
notify({ type: 'error', message: t('importBundleFailed', { ns: 'app' }) })
}
}
catch (e) {
const error = e as Error
notify({ type: 'error', message: error.message || t('importBundleFailed', { ns: 'app' }) })
}
finally {
setIsImporting(false)
}
}
return (
<Modal
@ -43,7 +104,15 @@ const DSLConfirmModal = ({
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button
variant="primary"
destructive
onClick={handleConfirm}
disabled={confirmDisabled || isImporting}
loading={isImporting}
>
{t('newApp.Confirm', { ns: 'app' })}
</Button>
</div>
</Modal>
)

View File

@ -1,6 +1,5 @@
'use client'
import type { MouseEventHandler } from 'react'
import { RiCloseLine, RiExternalLinkLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
@ -24,11 +23,13 @@ import {
DSLImportStatus,
} from '@/models/app'
import {
importAppBundle,
importDSL,
importDSLConfirm,
} from '@/service/apps'
import { getRedirection } from '@/utils/app-redirection'
import { cn } from '@/utils/classnames'
import DSLConfirmModal from './dsl-confirm-modal'
import Uploader from './uploader'
type CreateFromDSLModalProps = {
@ -59,6 +60,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [importId, setImportId] = useState<string>()
const { handleCheckPluginDependencies } = usePluginDependencies()
const isZipFile = (file?: File) => !!file && file.name.toLowerCase().endsWith('.zip')
const readFile = (file: File) => {
const reader = new FileReader()
reader.onload = function (event) {
@ -70,9 +73,9 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const handleFile = (file?: File) => {
setDSLFile(file)
if (file)
if (file && !isZipFile(file))
readFile(file)
if (!file)
if (!file || isZipFile(file))
setFileContent('')
}
@ -99,10 +102,15 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
let response
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
response = await importDSL({
mode: DSLImportMode.YAML_CONTENT,
yaml_content: fileContent || '',
})
if (isZipFile(currentFile)) {
response = await importAppBundle({ file: currentFile! })
}
else {
response = await importDSL({
mode: DSLImportMode.YAML_CONTENT,
yaml_content: fileContent || '',
})
}
}
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
response = await importDSL({
@ -170,7 +178,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
onClose()
})
const onDSLConfirm: MouseEventHandler = async () => {
const onDSLConfirm = async () => {
try {
if (!importId)
return
@ -205,6 +213,12 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
}
}
const handleConfirmSuccess = () => {
if (onSuccess)
onSuccess()
onClose()
}
const tabs = [
{
key: CreateFromDSLModalTab.FROM_FILE,
@ -273,6 +287,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
className="mt-0"
file={currentFile}
updateFile={handleFile}
accept=".yaml,.yml,.zip"
displayName={isZipFile(currentFile) ? 'ZIP' : 'YAML'}
/>
)}
{currentTab === CreateFromDSLModalTab.FROM_URL && (
@ -317,32 +333,15 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
</div>
</div>
</Modal>
<Modal
isShow={showErrorModal}
onClose={() => setShowErrorModal(false)}
className="w-[480px]"
>
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
<div className="system-md-regular flex grow flex-col text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions?.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</div>
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" destructive onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
{showErrorModal && (
<DSLConfirmModal
file={currentFile}
versions={versions}
onCancel={() => setShowErrorModal(false)}
onConfirm={onDSLConfirm}
onSuccess={handleConfirmSuccess}
/>
)}
</>
)
}