Co-authored-by: StyleZhang <jasonapring2015@outlook.com>tags/0.6.12
| @@ -109,6 +109,34 @@ class DraftWorkflowApi(Resource): | |||
| } | |||
| class DraftWorkflowImportApi(Resource): | |||
| @setup_required | |||
| @login_required | |||
| @account_initialization_required | |||
| @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) | |||
| @marshal_with(workflow_fields) | |||
| def post(self, app_model: App): | |||
| """ | |||
| Import draft workflow | |||
| """ | |||
| # The role of the current user in the ta table must be admin, owner, or editor | |||
| if not current_user.is_editor: | |||
| raise Forbidden() | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('data', type=str, required=True, nullable=False, location='json') | |||
| args = parser.parse_args() | |||
| workflow_service = WorkflowService() | |||
| workflow = workflow_service.import_draft_workflow( | |||
| app_model=app_model, | |||
| data=args['data'], | |||
| account=current_user | |||
| ) | |||
| return workflow | |||
| class AdvancedChatDraftWorkflowRunApi(Resource): | |||
| @setup_required | |||
| @login_required | |||
| @@ -439,6 +467,7 @@ class ConvertToWorkflowApi(Resource): | |||
| api.add_resource(DraftWorkflowApi, '/apps/<uuid:app_id>/workflows/draft') | |||
| api.add_resource(DraftWorkflowImportApi, '/apps/<uuid:app_id>/workflows/draft/import') | |||
| api.add_resource(AdvancedChatDraftWorkflowRunApi, '/apps/<uuid:app_id>/advanced-chat/workflows/draft/run') | |||
| api.add_resource(DraftWorkflowRunApi, '/apps/<uuid:app_id>/workflows/draft/run') | |||
| api.add_resource(WorkflowTaskStopApi, '/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop') | |||
| @@ -3,6 +3,8 @@ import time | |||
| from datetime import datetime, timezone | |||
| from typing import Optional | |||
| import yaml | |||
| from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager | |||
| from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager | |||
| from core.model_runtime.utils.encoders import jsonable_encoder | |||
| @@ -112,6 +114,56 @@ class WorkflowService: | |||
| # return draft workflow | |||
| return workflow | |||
| def import_draft_workflow(self, app_model: App, | |||
| data: str, | |||
| account: Account) -> Workflow: | |||
| """ | |||
| Import draft workflow | |||
| :param app_model: App instance | |||
| :param data: import data | |||
| :param account: Account instance | |||
| :return: | |||
| """ | |||
| try: | |||
| import_data = yaml.safe_load(data) | |||
| except yaml.YAMLError as e: | |||
| raise ValueError("Invalid YAML format in data argument.") | |||
| app_data = import_data.get('app') | |||
| workflow = import_data.get('workflow') | |||
| if not app_data: | |||
| raise ValueError("Missing app in data argument") | |||
| app_mode = AppMode.value_of(app_data.get('mode')) | |||
| if app_mode not in [AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]: | |||
| raise ValueError("Only support import workflow in advanced-chat or workflow app.") | |||
| if app_data.get('mode') != app_model.mode: | |||
| raise ValueError(f"App mode {app_data.get('mode')} is not matched with current app mode {app_model.mode}") | |||
| if not workflow: | |||
| raise ValueError("Missing workflow in data argument " | |||
| "when app mode is advanced-chat or workflow") | |||
| # fetch draft workflow by app_model | |||
| current_draft_workflow = self.get_draft_workflow(app_model=app_model) | |||
| if current_draft_workflow: | |||
| unique_hash = current_draft_workflow.unique_hash | |||
| else: | |||
| unique_hash = None | |||
| # sync draft workflow | |||
| draft_workflow = self.sync_draft_workflow( | |||
| app_model=app_model, | |||
| graph=workflow.get('graph'), | |||
| features=workflow.get('features'), | |||
| unique_hash=unique_hash, | |||
| account=account | |||
| ) | |||
| return draft_workflow | |||
| def publish_workflow(self, app_model: App, | |||
| account: Account, | |||
| draft_workflow: Optional[Workflow] = None) -> Workflow: | |||
| @@ -27,6 +27,7 @@ import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTrave | |||
| import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| import { getRedirection } from '@/utils/app-redirection' | |||
| import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal' | |||
| export type IAppInfoProps = { | |||
| expand: boolean | |||
| @@ -45,6 +46,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| const [showConfirmDelete, setShowConfirmDelete] = useState(false) | |||
| const [showSwitchTip, setShowSwitchTip] = useState<string>('') | |||
| const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false) | |||
| const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false) | |||
| const mutateApps = useContextSelector( | |||
| AppsContext, | |||
| @@ -295,9 +297,6 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| }}> | |||
| <span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span> | |||
| </div> | |||
| <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}> | |||
| <span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span> | |||
| </div> | |||
| {(appDetail.mode === 'completion' || appDetail.mode === 'chat') && ( | |||
| <> | |||
| <Divider className="!my-1" /> | |||
| @@ -315,6 +314,22 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| </> | |||
| )} | |||
| <Divider className="!my-1" /> | |||
| <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}> | |||
| <span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span> | |||
| </div> | |||
| { | |||
| (appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && ( | |||
| <div | |||
| className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' | |||
| onClick={() => { | |||
| setOpen(false) | |||
| setShowImportDSLModal(true) | |||
| }}> | |||
| <span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span> | |||
| </div> | |||
| ) | |||
| } | |||
| <Divider className="!my-1" /> | |||
| <div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => { | |||
| setOpen(false) | |||
| setShowConfirmDelete(true) | |||
| @@ -388,6 +403,14 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| onCancel={() => setShowConfirmDelete(false)} | |||
| /> | |||
| )} | |||
| { | |||
| showImportDSLModal && ( | |||
| <UpdateDSLModal | |||
| onCancel={() => setShowImportDSLModal(false)} | |||
| onBackup={onExport} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| </PortalToFollowElem> | |||
| ) | |||
| @@ -15,11 +15,13 @@ import Button from '@/app/components/base/button' | |||
| export type Props = { | |||
| file: File | undefined | |||
| updateFile: (file?: File) => void | |||
| className?: string | |||
| } | |||
| const Uploader: FC<Props> = ({ | |||
| file, | |||
| updateFile, | |||
| className, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| @@ -83,7 +85,7 @@ const Uploader: FC<Props> = ({ | |||
| }, []) | |||
| return ( | |||
| <div className='mt-6'> | |||
| <div className={cn('mt-6', className)}> | |||
| <input | |||
| ref={fileUploader} | |||
| style={{ display: 'none' }} | |||
| @@ -1,4 +1,5 @@ | |||
| import { useCallback } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useReactFlow } from 'reactflow' | |||
| import { useWorkflowStore } from '../store' | |||
| import { WORKFLOW_DATA_UPDATE } from '../constants' | |||
| @@ -11,6 +12,9 @@ import { useEdgesInteractions } from './use-edges-interactions' | |||
| import { useNodesInteractions } from './use-nodes-interactions' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import { fetchWorkflowDraft } from '@/service/workflow' | |||
| import { exportAppConfig } from '@/service/apps' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| export const useWorkflowInteractions = () => { | |||
| const workflowStore = useWorkflowStore() | |||
| @@ -71,3 +75,29 @@ export const useWorkflowUpdate = () => { | |||
| handleRefreshWorkflowDraft, | |||
| } | |||
| } | |||
| export const useDSL = () => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useToastContext() | |||
| const appDetail = useAppStore(s => s.appDetail) | |||
| const handleExportDSL = useCallback(async () => { | |||
| if (!appDetail) | |||
| return | |||
| try { | |||
| const { data } = await exportAppConfig(appDetail.id) | |||
| const a = document.createElement('a') | |||
| const file = new Blob([data], { type: 'application/yaml' }) | |||
| a.href = URL.createObjectURL(file) | |||
| a.download = `${appDetail.name}.yml` | |||
| a.click() | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('app.exportFailed') }) | |||
| } | |||
| }, [appDetail, notify, t]) | |||
| return { | |||
| handleExportDSL, | |||
| } | |||
| } | |||
| @@ -20,6 +20,7 @@ import ReactFlow, { | |||
| useEdgesState, | |||
| useNodesState, | |||
| useOnViewportChange, | |||
| useReactFlow, | |||
| } from 'reactflow' | |||
| import type { | |||
| Viewport, | |||
| @@ -32,6 +33,7 @@ import type { | |||
| } from './types' | |||
| import { WorkflowContextProvider } from './context' | |||
| import { | |||
| useDSL, | |||
| useEdgesInteractions, | |||
| useNodesInteractions, | |||
| useNodesReadOnly, | |||
| @@ -58,6 +60,7 @@ import CandidateNode from './candidate-node' | |||
| import PanelContextmenu from './panel-contextmenu' | |||
| import NodeContextmenu from './node-contextmenu' | |||
| import SyncingDataModal from './syncing-data-modal' | |||
| import UpdateDSLModal from './update-dsl-modal' | |||
| import { | |||
| useStore, | |||
| useWorkflowStore, | |||
| @@ -76,6 +79,7 @@ import { | |||
| import Loading from '@/app/components/base/loading' | |||
| import { FeaturesProvider } from '@/app/components/base/features' | |||
| import type { Features as FeaturesData } from '@/app/components/base/features/types' | |||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import Confirm from '@/app/components/base/confirm/common' | |||
| @@ -99,15 +103,20 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| }) => { | |||
| const workflowContainerRef = useRef<HTMLDivElement>(null) | |||
| const workflowStore = useWorkflowStore() | |||
| const reactflow = useReactFlow() | |||
| const featuresStore = useFeaturesStore() | |||
| const [nodes, setNodes] = useNodesState(originalNodes) | |||
| const [edges, setEdges] = useEdgesState(originalEdges) | |||
| const showFeaturesPanel = useStore(state => state.showFeaturesPanel) | |||
| const controlMode = useStore(s => s.controlMode) | |||
| const nodeAnimation = useStore(s => s.nodeAnimation) | |||
| const showConfirm = useStore(s => s.showConfirm) | |||
| const showImportDSLModal = useStore(s => s.showImportDSLModal) | |||
| const { | |||
| setShowConfirm, | |||
| setControlPromptEditorRerenderKey, | |||
| setShowImportDSLModal, | |||
| setSyncWorkflowDraftHash, | |||
| } = workflowStore.getState() | |||
| const { | |||
| handleSyncWorkflowDraft, | |||
| @@ -122,6 +131,19 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| if (v.type === WORKFLOW_DATA_UPDATE) { | |||
| setNodes(v.payload.nodes) | |||
| setEdges(v.payload.edges) | |||
| if (v.payload.viewport) | |||
| reactflow.setViewport(v.payload.viewport) | |||
| if (v.payload.features && featuresStore) { | |||
| const { setFeatures } = featuresStore.getState() | |||
| setFeatures(v.payload.features) | |||
| } | |||
| if (v.payload.hash) | |||
| setSyncWorkflowDraftHash(v.payload.hash) | |||
| setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) | |||
| } | |||
| }) | |||
| @@ -204,11 +226,15 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| } = useSelectionInteractions() | |||
| const { | |||
| handlePaneContextMenu, | |||
| handlePaneContextmenuCancel, | |||
| } = usePanelInteractions() | |||
| const { | |||
| isValidConnection, | |||
| } = useWorkflow() | |||
| const { handleStartWorkflowRun } = useWorkflowStartRun() | |||
| const { | |||
| handleExportDSL, | |||
| } = useDSL() | |||
| useOnViewportChange({ | |||
| onEnd: () => { | |||
| @@ -266,6 +292,15 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| showImportDSLModal && ( | |||
| <UpdateDSLModal | |||
| onCancel={() => setShowImportDSLModal(false)} | |||
| onBackup={handleExportDSL} | |||
| onImport={handlePaneContextmenuCancel} | |||
| /> | |||
| ) | |||
| } | |||
| <ReactFlow | |||
| nodeTypes={nodeTypes} | |||
| edgeTypes={edgeTypes} | |||
| @@ -8,48 +8,30 @@ import { useClickAway } from 'ahooks' | |||
| import ShortcutsName from './shortcuts-name' | |||
| import { useStore } from './store' | |||
| import { | |||
| useDSL, | |||
| useNodesInteractions, | |||
| usePanelInteractions, | |||
| useWorkflowStartRun, | |||
| } from './hooks' | |||
| import AddBlock from './operator/add-block' | |||
| import { useOperator } from './operator/hooks' | |||
| import { exportAppConfig } from '@/service/apps' | |||
| import { useToastContext } from '@/app/components/base/toast' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| const PanelContextmenu = () => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useToastContext() | |||
| const ref = useRef(null) | |||
| const panelMenu = useStore(s => s.panelMenu) | |||
| const clipboardElements = useStore(s => s.clipboardElements) | |||
| const appDetail = useAppStore(s => s.appDetail) | |||
| const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal) | |||
| const { handleNodesPaste } = useNodesInteractions() | |||
| const { handlePaneContextmenuCancel } = usePanelInteractions() | |||
| const { handleStartWorkflowRun } = useWorkflowStartRun() | |||
| const { handleAddNote } = useOperator() | |||
| const { handleExportDSL } = useDSL() | |||
| useClickAway(() => { | |||
| handlePaneContextmenuCancel() | |||
| }, ref) | |||
| const onExport = async () => { | |||
| if (!appDetail) | |||
| return | |||
| try { | |||
| const { data } = await exportAppConfig(appDetail.id) | |||
| const a = document.createElement('a') | |||
| const file = new Blob([data], { type: 'application/yaml' }) | |||
| a.href = URL.createObjectURL(file) | |||
| a.download = `${appDetail.name}.yml` | |||
| a.click() | |||
| } | |||
| catch (e) { | |||
| notify({ type: 'error', message: t('app.exportFailed') }) | |||
| } | |||
| } | |||
| const renderTrigger = () => { | |||
| return ( | |||
| <div | |||
| @@ -123,10 +105,16 @@ const PanelContextmenu = () => { | |||
| <div className='p-1'> | |||
| <div | |||
| className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50' | |||
| onClick={() => onExport()} | |||
| onClick={() => handleExportDSL()} | |||
| > | |||
| {t('app.export')} | |||
| </div> | |||
| <div | |||
| className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50' | |||
| onClick={() => setShowImportDSLModal(true)} | |||
| > | |||
| {t('workflow.common.importDSL')} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ) | |||
| @@ -129,6 +129,8 @@ type Shape = { | |||
| setIsSyncingWorkflowDraft: (isSyncingWorkflowDraft: boolean) => void | |||
| controlPromptEditorRerenderKey: number | |||
| setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void | |||
| showImportDSLModal: boolean | |||
| setShowImportDSLModal: (showImportDSLModal: boolean) => void | |||
| } | |||
| export const createWorkflowStore = () => { | |||
| @@ -217,6 +219,8 @@ export const createWorkflowStore = () => { | |||
| setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), | |||
| controlPromptEditorRerenderKey: 0, | |||
| setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })), | |||
| showImportDSLModal: false, | |||
| setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), | |||
| })) | |||
| } | |||
| @@ -0,0 +1,154 @@ | |||
| 'use client' | |||
| import type { MouseEventHandler } from 'react' | |||
| import { | |||
| memo, | |||
| useCallback, | |||
| useRef, | |||
| useState, | |||
| } from 'react' | |||
| import { useContext } from 'use-context-selector' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| RiAlertLine, | |||
| RiCloseLine, | |||
| } from '@remixicon/react' | |||
| import { WORKFLOW_DATA_UPDATE } from './constants' | |||
| import { | |||
| initialEdges, | |||
| initialNodes, | |||
| } from './utils' | |||
| import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' | |||
| import Button from '@/app/components/base/button' | |||
| import Modal from '@/app/components/base/modal' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import { updateWorkflowDraftFromDSL } from '@/service/workflow' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| type UpdateDSLModalProps = { | |||
| onCancel: () => void | |||
| onBackup: () => void | |||
| onImport?: () => void | |||
| } | |||
| const UpdateDSLModal = ({ | |||
| onCancel, | |||
| onBackup, | |||
| onImport, | |||
| }: UpdateDSLModalProps) => { | |||
| const { t } = useTranslation() | |||
| const { notify } = useContext(ToastContext) | |||
| const appDetail = useAppStore(s => s.appDetail) | |||
| const [currentFile, setDSLFile] = useState<File>() | |||
| const [fileContent, setFileContent] = useState<string>() | |||
| const [loading, setLoading] = useState(false) | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| const readFile = (file: File) => { | |||
| const reader = new FileReader() | |||
| reader.onload = function (event) { | |||
| const content = event.target?.result | |||
| setFileContent(content as string) | |||
| } | |||
| reader.readAsText(file) | |||
| } | |||
| const handleFile = (file?: File) => { | |||
| setDSLFile(file) | |||
| if (file) | |||
| readFile(file) | |||
| if (!file) | |||
| setFileContent('') | |||
| } | |||
| const isCreatingRef = useRef(false) | |||
| const handleImport: MouseEventHandler = useCallback(async () => { | |||
| if (isCreatingRef.current) | |||
| return | |||
| isCreatingRef.current = true | |||
| if (!currentFile) | |||
| return | |||
| try { | |||
| if (appDetail && fileContent) { | |||
| setLoading(true) | |||
| const { | |||
| graph, | |||
| features, | |||
| hash, | |||
| } = await updateWorkflowDraftFromDSL(appDetail.id, fileContent) | |||
| const { nodes, edges, viewport } = graph | |||
| eventEmitter?.emit({ | |||
| type: WORKFLOW_DATA_UPDATE, | |||
| payload: { | |||
| nodes: initialNodes(nodes, edges), | |||
| edges: initialEdges(edges, nodes), | |||
| viewport, | |||
| features, | |||
| hash, | |||
| }, | |||
| } as any) | |||
| if (onImport) | |||
| onImport() | |||
| notify({ type: 'success', message: t('workflow.common.importSuccess') }) | |||
| setLoading(false) | |||
| onCancel() | |||
| } | |||
| } | |||
| catch (e) { | |||
| setLoading(false) | |||
| notify({ type: 'error', message: t('workflow.common.importFailure') }) | |||
| } | |||
| isCreatingRef.current = false | |||
| }, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport]) | |||
| return ( | |||
| <Modal | |||
| className='p-6 w-[520px] rounded-2xl' | |||
| isShow={true} | |||
| onClose={() => {}} | |||
| > | |||
| <div className='flex items-center justify-between mb-6'> | |||
| <div className='text-2xl font-semibold text-[#101828]'>{t('workflow.common.importDSL')}</div> | |||
| <div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}> | |||
| <RiCloseLine className='w-5 h-5 text-gray-500' /> | |||
| </div> | |||
| </div> | |||
| <div className='flex mb-4 px-4 py-3 bg-[#FFFAEB] rounded-xl border border-[#FEDF89]'> | |||
| <RiAlertLine className='shrink-0 mt-0.5 mr-2 w-4 h-4 text-[#F79009]' /> | |||
| <div> | |||
| <div className='mb-2 text-sm font-medium text-[#354052]'>{t('workflow.common.importDSLTip')}</div> | |||
| <Button | |||
| variant='secondary-accent' | |||
| onClick={onBackup} | |||
| > | |||
| {t('workflow.common.backupCurrentDraft')} | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| <div className='mb-8'> | |||
| <div className='mb-1 text-[13px] font-semibold text-[#354052]'> | |||
| {t('workflow.common.chooseDSL')} | |||
| </div> | |||
| <Uploader | |||
| file={currentFile} | |||
| updateFile={handleFile} | |||
| className='!mt-0' | |||
| /> | |||
| </div> | |||
| <div className='flex justify-end'> | |||
| <Button className='mr-2' onClick={onCancel}>{t('app.newApp.Cancel')}</Button> | |||
| <Button | |||
| disabled={!currentFile || loading} | |||
| variant='warning' | |||
| onClick={handleImport} | |||
| loading={loading} | |||
| > | |||
| {t('workflow.common.overwriteAndImport')} | |||
| </Button> | |||
| </div> | |||
| </Modal> | |||
| ) | |||
| } | |||
| export default memo(UpdateDSLModal) | |||
| @@ -68,6 +68,13 @@ const translation = { | |||
| workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.', | |||
| viewDetailInTracingPanel: 'View details', | |||
| syncingData: 'Syncing data, just a few seconds.', | |||
| importDSL: 'Import DSL', | |||
| importDSLTip: 'Current draft will be overwritten. Export workflow as backup before importing.', | |||
| backupCurrentDraft: 'Backup Current Draft', | |||
| chooseDSL: 'Choose DSL(yml) file', | |||
| overwriteAndImport: 'Overwrite and Import', | |||
| importFailure: 'Import failure', | |||
| importSuccess: 'Import success', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} is required', | |||
| @@ -68,6 +68,13 @@ const translation = { | |||
| workflowAsToolTip: '工作流更新后需要重新配置工具参数', | |||
| viewDetailInTracingPanel: '查看详细信息', | |||
| syncingData: '同步数据中,只需几秒钟。', | |||
| importDSL: '导入 DSL', | |||
| importDSLTip: '当前草稿将被覆盖。在导入之前请导出工作流作为备份。', | |||
| backupCurrentDraft: '备份当前草稿', | |||
| chooseDSL: '选择 DSL(yml) 文件', | |||
| overwriteAndImport: '覆盖并导入', | |||
| importFailure: '导入失败', | |||
| importSuccess: '导入成功', | |||
| }, | |||
| errorMsg: { | |||
| fieldRequired: '{{field}} 不能为空', | |||
| @@ -54,3 +54,7 @@ export const fetchNodeDefault = (appId: string, blockType: BlockEnum, query = {} | |||
| params: { q: JSON.stringify(query) }, | |||
| }) | |||
| } | |||
| export const updateWorkflowDraftFromDSL = (appId: string, data: string) => { | |||
| return post<FetchWorkflowDraftResponse>(`apps/${appId}/workflows/draft/import`, { body: { data } }) | |||
| } | |||