| @@ -0,0 +1,46 @@ | |||
| import { useTranslation } from 'react-i18next' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| type DSLConfirmModalProps = { | |||
| versions?: { | |||
| importedVersion: string | |||
| systemVersion: string | |||
| } | |||
| onCancel: () => void | |||
| onConfirm: () => void | |||
| confirmDisabled?: boolean | |||
| } | |||
| const DSLConfirmModal = ({ | |||
| versions = { importedVersion: '', systemVersion: '' }, | |||
| onCancel, | |||
| onConfirm, | |||
| confirmDisabled = false, | |||
| }: DSLConfirmModalProps) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <Modal | |||
| isShow | |||
| onClose={() => onCancel()} | |||
| 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('app.newApp.appCreateDSLErrorTitle')}</div> | |||
| <div className='system-md-regular flex grow flex-col text-text-secondary'> | |||
| <div>{t('app.newApp.appCreateDSLErrorPart1')}</div> | |||
| <div>{t('app.newApp.appCreateDSLErrorPart2')}</div> | |||
| <br /> | |||
| <div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div> | |||
| <div>{t('app.newApp.appCreateDSLErrorPart4')}<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={() => onCancel()}>{t('app.newApp.Cancel')}</Button> | |||
| <Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button> | |||
| </div> | |||
| </Modal> | |||
| ) | |||
| } | |||
| export default DSLConfirmModal | |||
| @@ -1,12 +1,10 @@ | |||
| 'use client' | |||
| import React, { useMemo, useState } from 'react' | |||
| import { useRouter } from 'next/navigation' | |||
| import React, { useCallback, useMemo, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import useSWR from 'swr' | |||
| import { useDebounceFn } from 'ahooks' | |||
| import Toast from '../../base/toast' | |||
| import s from './style.module.css' | |||
| import cn from '@/utils/classnames' | |||
| import ExploreContext from '@/context/explore-context' | |||
| @@ -14,17 +12,16 @@ import type { App } from '@/models/explore' | |||
| import Category from '@/app/components/explore/category' | |||
| import AppCard from '@/app/components/explore/app-card' | |||
| import { fetchAppDetail, fetchAppList } from '@/service/explore' | |||
| import { importDSL } from '@/service/apps' | |||
| import { useTabSearchParams } from '@/hooks/use-tab-searchparams' | |||
| import CreateAppModal from '@/app/components/explore/create-app-modal' | |||
| import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { getRedirection } from '@/utils/app-redirection' | |||
| import Input from '@/app/components/base/input' | |||
| import { DSLImportMode } from '@/models/app' | |||
| import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' | |||
| import { | |||
| DSLImportMode, | |||
| } from '@/models/app' | |||
| import { useImportDSL } from '@/hooks/use-import-dsl' | |||
| import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal' | |||
| type AppsProps = { | |||
| onSuccess?: () => void | |||
| @@ -39,8 +36,6 @@ const Apps = ({ | |||
| onSuccess, | |||
| }: AppsProps) => { | |||
| const { t } = useTranslation() | |||
| const { isCurrentWorkspaceEditor } = useAppContext() | |||
| const { push } = useRouter() | |||
| const { hasEditPermission } = useContext(ExploreContext) | |||
| const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' }) | |||
| @@ -115,7 +110,14 @@ const Apps = ({ | |||
| const [currApp, setCurrApp] = React.useState<App | null>(null) | |||
| const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) | |||
| const { handleCheckPluginDependencies } = usePluginDependencies() | |||
| const { | |||
| handleImportDSL, | |||
| handleImportDSLConfirm, | |||
| versions, | |||
| isFetching, | |||
| } = useImportDSL() | |||
| const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false) | |||
| const onCreate: CreateAppModalProps['onConfirm'] = async ({ | |||
| name, | |||
| icon_type, | |||
| @@ -123,36 +125,34 @@ const Apps = ({ | |||
| icon_background, | |||
| description, | |||
| }) => { | |||
| const { export_data, mode } = await fetchAppDetail( | |||
| const { export_data } = await fetchAppDetail( | |||
| currApp?.app.id as string, | |||
| ) | |||
| try { | |||
| const app = await importDSL({ | |||
| mode: DSLImportMode.YAML_CONTENT, | |||
| yaml_content: export_data, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| }) | |||
| setIsShowCreateModal(false) | |||
| Toast.notify({ | |||
| type: 'success', | |||
| message: t('app.newApp.appCreated'), | |||
| }) | |||
| if (onSuccess) | |||
| onSuccess() | |||
| if (app.app_id) | |||
| await handleCheckPluginDependencies(app.app_id) | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push) | |||
| } | |||
| catch { | |||
| Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| const payload = { | |||
| mode: DSLImportMode.YAML_CONTENT, | |||
| yaml_content: export_data, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| } | |||
| await handleImportDSL(payload, { | |||
| onSuccess: () => { | |||
| setIsShowCreateModal(false) | |||
| }, | |||
| onPending: () => { | |||
| setShowDSLConfirmModal(true) | |||
| }, | |||
| }) | |||
| } | |||
| const onConfirmDSL = useCallback(async () => { | |||
| await handleImportDSLConfirm({ | |||
| onSuccess, | |||
| }) | |||
| }, [handleImportDSLConfirm, onSuccess]) | |||
| if (!categories || categories.length === 0) { | |||
| return ( | |||
| <div className="flex h-full items-center"> | |||
| @@ -225,9 +225,20 @@ const Apps = ({ | |||
| appDescription={currApp?.app.description || ''} | |||
| show={isShowCreateModal} | |||
| onConfirm={onCreate} | |||
| confirmDisabled={isFetching} | |||
| onHide={() => setIsShowCreateModal(false)} | |||
| /> | |||
| )} | |||
| { | |||
| showDSLConfirmModal && ( | |||
| <DSLConfirmModal | |||
| versions={versions} | |||
| onCancel={() => setShowDSLConfirmModal(false)} | |||
| onConfirm={onConfirmDSL} | |||
| confirmDisabled={isFetching} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -35,6 +35,7 @@ export type CreateAppModalProps = { | |||
| description: string | |||
| use_icon_as_answer_icon?: boolean | |||
| }) => Promise<void> | |||
| confirmDisabled?: boolean | |||
| onHide: () => void | |||
| } | |||
| @@ -50,6 +51,7 @@ const CreateAppModal = ({ | |||
| appMode, | |||
| appUseIconAsAnswerIcon, | |||
| onConfirm, | |||
| confirmDisabled, | |||
| onHide, | |||
| }: CreateAppModalProps) => { | |||
| const { t } = useTranslation() | |||
| @@ -160,7 +162,7 @@ const CreateAppModal = ({ | |||
| </div> | |||
| <div className='flex flex-row-reverse'> | |||
| <Button | |||
| disabled={(!isEditModal && isAppsFull) || !name.trim()} | |||
| disabled={(!isEditModal && isAppsFull) || !name.trim() || confirmDisabled} | |||
| className='ml-2 w-24 gap-1' | |||
| variant='primary' | |||
| onClick={handleSubmit} | |||
| @@ -0,0 +1,163 @@ | |||
| import { | |||
| useCallback, | |||
| useRef, | |||
| useState, | |||
| } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useRouter } from 'next/navigation' | |||
| import type { | |||
| DSLImportMode, | |||
| DSLImportResponse, | |||
| } from '@/models/app' | |||
| import { DSLImportStatus } from '@/models/app' | |||
| import { | |||
| importDSL, | |||
| importDSLConfirm, | |||
| } from '@/service/apps' | |||
| import type { AppIconType } from '@/types/app' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' | |||
| import { getRedirection } from '@/utils/app-redirection' | |||
| import { useSelector } from '@/context/app-context' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| type DSLPayload = { | |||
| mode: DSLImportMode | |||
| yaml_content?: string | |||
| yaml_url?: string | |||
| name?: string | |||
| icon_type?: AppIconType | |||
| icon?: string | |||
| icon_background?: string | |||
| description?: string | |||
| } | |||
| type ResponseCallback = { | |||
| onSuccess?: () => void | |||
| onPending?: (payload: DSLImportResponse) => void | |||
| onFailed?: () => void | |||
| } | |||
| export const useImportDSL = () => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useToastContext() | |||
| const [isFetching, setIsFetching] = useState(false) | |||
| const { handleCheckPluginDependencies } = usePluginDependencies() | |||
| const isCurrentWorkspaceEditor = useSelector(s => s.isCurrentWorkspaceEditor) | |||
| const { push } = useRouter() | |||
| const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>() | |||
| const importIdRef = useRef<string>('') | |||
| const handleImportDSL = useCallback(async ( | |||
| payload: DSLPayload, | |||
| { | |||
| onSuccess, | |||
| onPending, | |||
| onFailed, | |||
| }: ResponseCallback, | |||
| ) => { | |||
| if (isFetching) | |||
| return | |||
| setIsFetching(true) | |||
| try { | |||
| const response = await importDSL(payload) | |||
| if (!response) | |||
| return | |||
| const { | |||
| id, | |||
| status, | |||
| app_id, | |||
| app_mode, | |||
| imported_dsl_version, | |||
| current_dsl_version, | |||
| } = response | |||
| if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) { | |||
| if (!app_id) | |||
| return | |||
| notify({ | |||
| type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', | |||
| message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'), | |||
| children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'), | |||
| }) | |||
| onSuccess?.() | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| await handleCheckPluginDependencies(app_id) | |||
| getRedirection(isCurrentWorkspaceEditor, { id: app_id, mode: app_mode }, push) | |||
| } | |||
| else if (status === DSLImportStatus.PENDING) { | |||
| setVersions({ | |||
| importedVersion: imported_dsl_version ?? '', | |||
| systemVersion: current_dsl_version ?? '', | |||
| }) | |||
| importIdRef.current = id | |||
| onPending?.(response) | |||
| } | |||
| else { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| onFailed?.() | |||
| } | |||
| } | |||
| catch { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| onFailed?.() | |||
| } | |||
| finally { | |||
| setIsFetching(false) | |||
| } | |||
| }, [t, notify, handleCheckPluginDependencies, isCurrentWorkspaceEditor, push, isFetching]) | |||
| const handleImportDSLConfirm = useCallback(async ( | |||
| { | |||
| onSuccess, | |||
| onFailed, | |||
| }: Pick<ResponseCallback, 'onSuccess' | 'onFailed'>, | |||
| ) => { | |||
| if (isFetching) | |||
| return | |||
| setIsFetching(true) | |||
| if (!importIdRef.current) | |||
| return | |||
| try { | |||
| const response = await importDSLConfirm({ | |||
| import_id: importIdRef.current, | |||
| }) | |||
| const { status, app_id, app_mode } = response | |||
| if (!app_id) | |||
| return | |||
| if (status === DSLImportStatus.COMPLETED) { | |||
| onSuccess?.() | |||
| notify({ | |||
| type: 'success', | |||
| message: t('app.newApp.appCreated'), | |||
| }) | |||
| await handleCheckPluginDependencies(app_id) | |||
| localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') | |||
| getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push) | |||
| } | |||
| else if (status === DSLImportStatus.FAILED) { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| onFailed?.() | |||
| } | |||
| } | |||
| catch { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| onFailed?.() | |||
| } | |||
| finally { | |||
| setIsFetching(false) | |||
| } | |||
| }, [t, notify, handleCheckPluginDependencies, isCurrentWorkspaceEditor, push, isFetching]) | |||
| return { | |||
| handleImportDSL, | |||
| handleImportDSLConfirm, | |||
| versions, | |||
| isFetching, | |||
| } | |||
| } | |||