diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index d0cc7ff91..2aa192fb0 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import { RiApps2Line, + RiDragDropLine, RiExchange2Line, RiFile4Line, RiMessage3Line, @@ -16,7 +17,8 @@ import { } from '@remixicon/react' import AppCard from './AppCard' import NewAppCard from './NewAppCard' -import useAppsQueryState from './hooks/useAppsQueryState' +import useAppsQueryState from './hooks/use-apps-query-state' +import { useDSLDragDrop } from './hooks/use-dsl-drag-drop' import type { AppListResponse } from '@/models/app' import { fetchAppList } from '@/service/apps' import { useAppContext } from '@/context/app-context' @@ -29,6 +31,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st import TagManagementModal from '@/app/components/base/tag-management' import TagFilter from '@/app/components/base/tag-management/filter' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' +import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal' const getKey = ( pageIndex: number, @@ -67,6 +70,9 @@ const Apps = () => { const [tagFilterValue, setTagFilterValue] = useState(tagIDs) const [searchKeywords, setSearchKeywords] = useState(keywords) const newAppCardRef = useRef(null) + const containerRef = useRef(null) + const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) + const [droppedDSLFile, setDroppedDSLFile] = useState() const setKeywords = useCallback((keywords: string) => { setQuery(prev => ({ ...prev, keywords })) }, [setQuery]) @@ -74,6 +80,17 @@ const Apps = () => { setQuery(prev => ({ ...prev, tagIDs })) }, [setQuery]) + const handleDSLFileDropped = useCallback((file: File) => { + setDroppedDSLFile(file) + setShowCreateFromDSLModal(true) + }, []) + + const { dragging } = useDSLDragDrop({ + onDSLFileDropped: handleDSLFileDropped, + containerRef, + enabled: isCurrentWorkspaceEditor, + }) + const { data, isLoading, error, setSize, mutate } = useSWRInfinite( (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords), fetchAppList, @@ -151,47 +168,81 @@ const Apps = () => { return ( <> -
- -
- - - handleKeywordsChange(e.target.value)} - onClear={() => handleKeywordsChange('')} +
+ {dragging && ( +
+
+ )} + +
+ +
+ + + handleKeywordsChange(e.target.value)} + onClear={() => handleKeywordsChange('')} + /> +
+ {(data && data[0].total > 0) + ?
+ {isCurrentWorkspaceEditor + && } + {data.map(({ data: apps }) => apps.map(app => ( + + )))} +
+ :
+ {isCurrentWorkspaceEditor + && } + +
} + + {isCurrentWorkspaceEditor && ( +
+ + {t('app.newApp.dropDSLToCreateApp')} +
+ )} + +
+ {showTagManagementModal && ( + + )}
- {(data && data[0].total > 0) - ?
- {isCurrentWorkspaceEditor - && } - {data.map(({ data: apps }) => apps.map(app => ( - - )))} -
- :
- {isCurrentWorkspaceEditor - && } - -
} - -
- {showTagManagementModal && ( - + + {showCreateFromDSLModal && ( + { + setShowCreateFromDSLModal(false) + setDroppedDSLFile(undefined) + }} + onSuccess={() => { + setShowCreateFromDSLModal(false) + setDroppedDSLFile(undefined) + mutate() + }} + droppedFile={droppedDSLFile} + /> )} ) diff --git a/web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts b/web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts similarity index 100% rename from web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts rename to web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts diff --git a/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts b/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts new file mode 100644 index 000000000..96942ec54 --- /dev/null +++ b/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts @@ -0,0 +1,72 @@ +import { useEffect, useState } from 'react' + +type DSLDragDropHookProps = { + onDSLFileDropped: (file: File) => void + containerRef: React.RefObject + enabled?: boolean +} + +export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true }: DSLDragDropHookProps) => { + const [dragging, setDragging] = useState(false) + + const handleDragEnter = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.dataTransfer?.types.includes('Files')) + setDragging(true) + } + + const handleDragOver = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + } + + const handleDragLeave = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + if (e.relatedTarget === null || !containerRef.current?.contains(e.relatedTarget as Node)) + setDragging(false) + } + + const handleDrop = (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragging(false) + + if (!e.dataTransfer) + return + + const files = [...e.dataTransfer.files] + if (files.length === 0) + return + + const file = files[0] + if (file.name.toLowerCase().endsWith('.yaml') || file.name.toLowerCase().endsWith('.yml')) + onDSLFileDropped(file) + } + + useEffect(() => { + if (!enabled) + return + + const current = containerRef.current + if (current) { + current.addEventListener('dragenter', handleDragEnter) + current.addEventListener('dragover', handleDragOver) + current.addEventListener('dragleave', handleDragLeave) + current.addEventListener('drop', handleDrop) + } + return () => { + if (current) { + current.removeEventListener('dragenter', handleDragEnter) + current.removeEventListener('dragover', handleDragOver) + current.removeEventListener('dragleave', handleDragLeave) + current.removeEventListener('drop', handleDrop) + } + } + }, [containerRef, enabled]) + + return { + dragging: enabled ? dragging : false, + } +} diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 9739ac47e..8faafe05a 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { MouseEventHandler } from 'react' -import { useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' @@ -35,6 +35,7 @@ type CreateFromDSLModalProps = { onClose: () => void activeTab?: string dslUrl?: string + droppedFile?: File } export enum CreateFromDSLModalTab { @@ -42,11 +43,11 @@ export enum CreateFromDSLModalTab { FROM_URL = 'from-url', } -const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '' }: CreateFromDSLModalProps) => { +const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '', droppedFile }: CreateFromDSLModalProps) => { const { push } = useRouter() const { t } = useTranslation() const { notify } = useContext(ToastContext) - const [currentFile, setDSLFile] = useState() + const [currentFile, setDSLFile] = useState(droppedFile) const [fileContent, setFileContent] = useState() const [currentTab, setCurrentTab] = useState(activeTab) const [dslUrlValue, setDslUrlValue] = useState(dslUrl) @@ -78,6 +79,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS const isCreatingRef = useRef(false) + useEffect(() => { + if (droppedFile) + handleFile(droppedFile) + }, [droppedFile]) + const onCreate: MouseEventHandler = async () => { if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) return diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index ccfe23ead..e75a9d535 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -87,6 +87,7 @@ const translation = { appCreateDSLErrorPart3: 'Current application DSL version: ', appCreateDSLErrorPart4: 'System-supported DSL version: ', appCreateFailed: 'Failed to create app', + dropDSLToCreateApp: 'Drop DSL file here to create app', }, newAppFromTemplate: { byCategories: 'BY CATEGORIES', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 4ec1e6505..c5bfb39f4 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -87,6 +87,7 @@ const translation = { appCreateDSLErrorPart3: '当前应用 DSL 版本:', appCreateDSLErrorPart4: '系统支持 DSL 版本:', appCreateFailed: '应用创建失败', + dropDSLToCreateApp: '拖放 DSL 文件到此处创建应用', Confirm: '确认', }, newAppFromTemplate: {