feat: Support drop DSL file into the browser to create app (#20706)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useDebounceFn } from 'ahooks'
|
import { useDebounceFn } from 'ahooks'
|
||||||
import {
|
import {
|
||||||
RiApps2Line,
|
RiApps2Line,
|
||||||
|
RiDragDropLine,
|
||||||
RiExchange2Line,
|
RiExchange2Line,
|
||||||
RiFile4Line,
|
RiFile4Line,
|
||||||
RiMessage3Line,
|
RiMessage3Line,
|
||||||
@@ -16,7 +17,8 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import AppCard from './AppCard'
|
import AppCard from './AppCard'
|
||||||
import NewAppCard from './NewAppCard'
|
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 type { AppListResponse } from '@/models/app'
|
||||||
import { fetchAppList } from '@/service/apps'
|
import { fetchAppList } from '@/service/apps'
|
||||||
import { useAppContext } from '@/context/app-context'
|
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 TagManagementModal from '@/app/components/base/tag-management'
|
||||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||||
|
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
||||||
|
|
||||||
const getKey = (
|
const getKey = (
|
||||||
pageIndex: number,
|
pageIndex: number,
|
||||||
@@ -67,6 +70,9 @@ const Apps = () => {
|
|||||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
|
const [tagFilterValue, setTagFilterValue] = useState<string[]>(tagIDs)
|
||||||
const [searchKeywords, setSearchKeywords] = useState(keywords)
|
const [searchKeywords, setSearchKeywords] = useState(keywords)
|
||||||
const newAppCardRef = useRef<HTMLDivElement>(null)
|
const newAppCardRef = useRef<HTMLDivElement>(null)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||||
|
const [droppedDSLFile, setDroppedDSLFile] = useState<File | undefined>()
|
||||||
const setKeywords = useCallback((keywords: string) => {
|
const setKeywords = useCallback((keywords: string) => {
|
||||||
setQuery(prev => ({ ...prev, keywords }))
|
setQuery(prev => ({ ...prev, keywords }))
|
||||||
}, [setQuery])
|
}, [setQuery])
|
||||||
@@ -74,6 +80,17 @@ const Apps = () => {
|
|||||||
setQuery(prev => ({ ...prev, tagIDs }))
|
setQuery(prev => ({ ...prev, tagIDs }))
|
||||||
}, [setQuery])
|
}, [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(
|
const { data, isLoading, error, setSize, mutate } = useSWRInfinite(
|
||||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
|
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, isCreatedByMe, tagIDs, searchKeywords),
|
||||||
fetchAppList,
|
fetchAppList,
|
||||||
@@ -151,6 +168,12 @@ const Apps = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div ref={containerRef} className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
|
||||||
|
{dragging && (
|
||||||
|
<div className="absolute inset-0 z-50 m-0.5 rounded-2xl border-2 border-dashed border-components-dropzone-border-accent bg-[rgba(21,90,239,0.14)] p-2">
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
|
<div className='sticky top-0 z-10 flex flex-wrap items-center justify-between gap-y-2 bg-background-body px-12 pb-2 pt-4 leading-[56px]'>
|
||||||
<TabSliderNew
|
<TabSliderNew
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
@@ -188,11 +211,39 @@ const Apps = () => {
|
|||||||
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />}
|
&& <NewAppCard ref={newAppCardRef} className='z-10' onSuccess={mutate} />}
|
||||||
<NoAppsFound />
|
<NoAppsFound />
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
{isCurrentWorkspaceEditor && (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-center gap-2 py-4 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}
|
||||||
|
role="region"
|
||||||
|
aria-label={t('app.newApp.dropDSLToCreateApp')}
|
||||||
|
>
|
||||||
|
<RiDragDropLine className="h-4 w-4" />
|
||||||
|
<span className="system-xs-regular">{t('app.newApp.dropDSLToCreateApp')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<CheckModal />
|
<CheckModal />
|
||||||
<div ref={anchorRef} className='h-0'> </div>
|
<div ref={anchorRef} className='h-0'> </div>
|
||||||
{showTagManagementModal && (
|
{showTagManagementModal && (
|
||||||
<TagManagementModal type='app' show={showTagManagementModal} />
|
<TagManagementModal type='app' show={showTagManagementModal} />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCreateFromDSLModal && (
|
||||||
|
<CreateFromDSLModal
|
||||||
|
show={showCreateFromDSLModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowCreateFromDSLModal(false)
|
||||||
|
setDroppedDSLFile(undefined)
|
||||||
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
setShowCreateFromDSLModal(false)
|
||||||
|
setDroppedDSLFile(undefined)
|
||||||
|
mutate()
|
||||||
|
}}
|
||||||
|
droppedFile={droppedDSLFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
72
web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts
Normal file
72
web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
type DSLDragDropHookProps = {
|
||||||
|
onDSLFileDropped: (file: File) => void
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { MouseEventHandler } from 'react'
|
import type { MouseEventHandler } from 'react'
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -35,6 +35,7 @@ type CreateFromDSLModalProps = {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
activeTab?: string
|
activeTab?: string
|
||||||
dslUrl?: string
|
dslUrl?: string
|
||||||
|
droppedFile?: File
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CreateFromDSLModalTab {
|
export enum CreateFromDSLModalTab {
|
||||||
@@ -42,11 +43,11 @@ export enum CreateFromDSLModalTab {
|
|||||||
FROM_URL = 'from-url',
|
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 { push } = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const [currentFile, setDSLFile] = useState<File>()
|
const [currentFile, setDSLFile] = useState<File | undefined>(droppedFile)
|
||||||
const [fileContent, setFileContent] = useState<string>()
|
const [fileContent, setFileContent] = useState<string>()
|
||||||
const [currentTab, setCurrentTab] = useState(activeTab)
|
const [currentTab, setCurrentTab] = useState(activeTab)
|
||||||
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
|
||||||
@@ -78,6 +79,11 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||||||
|
|
||||||
const isCreatingRef = useRef(false)
|
const isCreatingRef = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (droppedFile)
|
||||||
|
handleFile(droppedFile)
|
||||||
|
}, [droppedFile])
|
||||||
|
|
||||||
const onCreate: MouseEventHandler = async () => {
|
const onCreate: MouseEventHandler = async () => {
|
||||||
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
|
||||||
return
|
return
|
||||||
|
@@ -87,6 +87,7 @@ const translation = {
|
|||||||
appCreateDSLErrorPart3: 'Current application DSL version: ',
|
appCreateDSLErrorPart3: 'Current application DSL version: ',
|
||||||
appCreateDSLErrorPart4: 'System-supported DSL version: ',
|
appCreateDSLErrorPart4: 'System-supported DSL version: ',
|
||||||
appCreateFailed: 'Failed to create app',
|
appCreateFailed: 'Failed to create app',
|
||||||
|
dropDSLToCreateApp: 'Drop DSL file here to create app',
|
||||||
},
|
},
|
||||||
newAppFromTemplate: {
|
newAppFromTemplate: {
|
||||||
byCategories: 'BY CATEGORIES',
|
byCategories: 'BY CATEGORIES',
|
||||||
|
@@ -87,6 +87,7 @@ const translation = {
|
|||||||
appCreateDSLErrorPart3: '当前应用 DSL 版本:',
|
appCreateDSLErrorPart3: '当前应用 DSL 版本:',
|
||||||
appCreateDSLErrorPart4: '系统支持 DSL 版本:',
|
appCreateDSLErrorPart4: '系统支持 DSL 版本:',
|
||||||
appCreateFailed: '应用创建失败',
|
appCreateFailed: '应用创建失败',
|
||||||
|
dropDSLToCreateApp: '拖放 DSL 文件到此处创建应用',
|
||||||
Confirm: '确认',
|
Confirm: '确认',
|
||||||
},
|
},
|
||||||
newAppFromTemplate: {
|
newAppFromTemplate: {
|
||||||
|
Reference in New Issue
Block a user