mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 23:45:55 +08:00
feat: add 'Open in Explore' link for each apps on studio (#11402)
This commit is contained in:
parent
061c0b10fd
commit
230fa3286b
@ -1,5 +1,6 @@
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restful import Resource, inputs, marshal_with, reqparse
|
from flask_restful import Resource, inputs, marshal_with, reqparse
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
@ -20,7 +21,16 @@ class InstalledAppsListApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(installed_app_list_fields)
|
@marshal_with(installed_app_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
app_id = request.args.get("app_id", default=None, type=str)
|
||||||
current_tenant_id = current_user.current_tenant_id
|
current_tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
|
if app_id:
|
||||||
|
installed_apps = (
|
||||||
|
db.session.query(InstalledApp)
|
||||||
|
.filter(and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
else:
|
||||||
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
|
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
|
||||||
|
|
||||||
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
||||||
|
@ -9,6 +9,7 @@ import s from './style.module.css'
|
|||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||||
@ -31,6 +32,7 @@ import TagSelector from '@/app/components/base/tag-management/selector'
|
|||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
|
|
||||||
export type AppCardProps = {
|
export type AppCardProps = {
|
||||||
app: App
|
app: App
|
||||||
@ -209,6 +211,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setShowConfirmDelete(true)
|
setShowConfirmDelete(true)
|
||||||
}
|
}
|
||||||
|
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
props.onClick?.()
|
||||||
|
e.preventDefault()
|
||||||
|
try {
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
|
||||||
|
if (installed_apps?.length > 0)
|
||||||
|
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||||
|
else
|
||||||
|
throw new Error('No app found in Explore')
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||||
<button className={s.actionItem} onClick={onClickSettings}>
|
<button className={s.actionItem} onClick={onClickSettings}>
|
||||||
@ -233,6 +250,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Divider className="!my-1" />
|
<Divider className="!my-1" />
|
||||||
|
<button className={s.actionItem} onClick={onClickInstalledApp}>
|
||||||
|
<span className={s.actionName}>{t('app.openInExplore')}</span>
|
||||||
|
</button>
|
||||||
|
<Divider className="!my-1" />
|
||||||
<div
|
<div
|
||||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||||
onClick={onClickDelete}
|
onClick={onClickDelete}
|
||||||
@ -353,10 +374,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
|||||||
}
|
}
|
||||||
popupClassName={
|
popupClassName={
|
||||||
(app.mode === 'completion' || app.mode === 'chat')
|
(app.mode === 'completion' || app.mode === 'chat')
|
||||||
? '!w-[238px] translate-x-[-110px]'
|
? '!w-[256px] translate-x-[-224px]'
|
||||||
: ''
|
: '!w-[160px] translate-x-[-128px]'
|
||||||
}
|
}
|
||||||
className={'!w-[128px] h-fit !z-20'}
|
className={'h-fit !z-20'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { RiArrowDownSLine } from '@remixicon/react'
|
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
|
||||||
|
import Toast from '../../base/toast'
|
||||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||||
import SuggestedAction from './suggested-action'
|
import SuggestedAction from './suggested-action'
|
||||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||||
@ -15,6 +16,7 @@ import {
|
|||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { useGetLanguage } from '@/context/i18n'
|
import { useGetLanguage } from '@/context/i18n'
|
||||||
@ -105,6 +107,19 @@ const AppPublisher = ({
|
|||||||
setPublished(false)
|
setPublished(false)
|
||||||
}, [disabled, onToggle, open])
|
}, [disabled, onToggle, open])
|
||||||
|
|
||||||
|
const handleOpenInExplore = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
|
||||||
|
if (installed_apps?.length > 0)
|
||||||
|
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
|
||||||
|
else
|
||||||
|
throw new Error('No app found in Explore')
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: `${e.message || e}` })
|
||||||
|
}
|
||||||
|
}, [appDetail?.id])
|
||||||
|
|
||||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -205,6 +220,15 @@ const AppPublisher = ({
|
|||||||
{t('workflow.common.embedIntoSite')}
|
{t('workflow.common.embedIntoSite')}
|
||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
)}
|
)}
|
||||||
|
<SuggestedAction
|
||||||
|
onClick={() => {
|
||||||
|
handleOpenInExplore()
|
||||||
|
}}
|
||||||
|
disabled={!publishedAt}
|
||||||
|
icon={<RiPlanetLine className='w-4 h-4' />}
|
||||||
|
>
|
||||||
|
{t('workflow.common.openInExplore')}
|
||||||
|
</SuggestedAction>
|
||||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||||
{appDetail?.mode === 'workflow' && (
|
{appDetail?.mode === 'workflow' && (
|
||||||
<WorkflowToolConfigureButton
|
<WorkflowToolConfigureButton
|
||||||
|
@ -101,6 +101,7 @@ const translation = {
|
|||||||
switchLabel: 'The app copy to be created',
|
switchLabel: 'The app copy to be created',
|
||||||
removeOriginal: 'Delete the original app',
|
removeOriginal: 'Delete the original app',
|
||||||
switchStart: 'Start switch',
|
switchStart: 'Start switch',
|
||||||
|
openInExplore: 'Open in Explore',
|
||||||
typeSelector: {
|
typeSelector: {
|
||||||
all: 'ALL Types',
|
all: 'ALL Types',
|
||||||
chatbot: 'Chatbot',
|
chatbot: 'Chatbot',
|
||||||
|
@ -32,6 +32,7 @@ const translation = {
|
|||||||
restore: 'Restore',
|
restore: 'Restore',
|
||||||
runApp: 'Run App',
|
runApp: 'Run App',
|
||||||
batchRunApp: 'Batch Run App',
|
batchRunApp: 'Batch Run App',
|
||||||
|
openInExplore: 'Open in Explore',
|
||||||
accessAPIReference: 'Access API Reference',
|
accessAPIReference: 'Access API Reference',
|
||||||
embedIntoSite: 'Embed Into Site',
|
embedIntoSite: 'Embed Into Site',
|
||||||
addTitle: 'Add title...',
|
addTitle: 'Add title...',
|
||||||
|
@ -93,6 +93,7 @@ const translation = {
|
|||||||
switchLabel: '作成されるアプリのコピー',
|
switchLabel: '作成されるアプリのコピー',
|
||||||
removeOriginal: '元のアプリを削除する',
|
removeOriginal: '元のアプリを削除する',
|
||||||
switchStart: '切り替えを開始する',
|
switchStart: '切り替えを開始する',
|
||||||
|
openInExplore: '"探索" で開く',
|
||||||
typeSelector: {
|
typeSelector: {
|
||||||
all: 'すべてのタイプ',
|
all: 'すべてのタイプ',
|
||||||
chatbot: 'チャットボット',
|
chatbot: 'チャットボット',
|
||||||
|
@ -32,6 +32,7 @@ const translation = {
|
|||||||
restore: '復元',
|
restore: '復元',
|
||||||
runApp: 'アプリを実行',
|
runApp: 'アプリを実行',
|
||||||
batchRunApp: 'バッチでアプリを実行',
|
batchRunApp: 'バッチでアプリを実行',
|
||||||
|
openInExplore: '"探索" で開く',
|
||||||
accessAPIReference: 'APIリファレンスにアクセス',
|
accessAPIReference: 'APIリファレンスにアクセス',
|
||||||
embedIntoSite: 'サイトに埋め込む',
|
embedIntoSite: 'サイトに埋め込む',
|
||||||
addTitle: 'タイトルを追加...',
|
addTitle: 'タイトルを追加...',
|
||||||
|
@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise<any> => {
|
|||||||
return get(`/explore/apps/${id}`)
|
return get(`/explore/apps/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchInstalledAppList = () => {
|
export const fetchInstalledAppList = (app_id?: string | null) => {
|
||||||
return get('/installed-apps')
|
return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const installApp = (id: string) => {
|
export const installApp = (id: string) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user