| 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_ | ||||
| @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 | ||||
| installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all() | |||||
| 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() | |||||
| 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) | ||||
| installed_apps = [ | installed_apps = [ |
| 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' | ||||
| 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 | ||||
| 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}> | ||||
| </> | </> | ||||
| )} | )} | ||||
| <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} | ||||
| } | } | ||||
| 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> | ||||
| </> | </> |
| } 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' | ||||
| 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' | ||||
| 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 ( | ||||
| {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 |
| 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', |
| 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...', |
| switchLabel: '作成されるアプリのコピー', | switchLabel: '作成されるアプリのコピー', | ||||
| removeOriginal: '元のアプリを削除する', | removeOriginal: '元のアプリを削除する', | ||||
| switchStart: '切り替えを開始する', | switchStart: '切り替えを開始する', | ||||
| openInExplore: '"探索" で開く', | |||||
| typeSelector: { | typeSelector: { | ||||
| all: 'すべてのタイプ', | all: 'すべてのタイプ', | ||||
| chatbot: 'チャットボット', | chatbot: 'チャットボット', |
| restore: '復元', | restore: '復元', | ||||
| runApp: 'アプリを実行', | runApp: 'アプリを実行', | ||||
| batchRunApp: 'バッチでアプリを実行', | batchRunApp: 'バッチでアプリを実行', | ||||
| openInExplore: '"探索" で開く', | |||||
| accessAPIReference: 'APIリファレンスにアクセス', | accessAPIReference: 'APIリファレンスにアクセス', | ||||
| embedIntoSite: 'サイトに埋め込む', | embedIntoSite: 'サイトに埋め込む', | ||||
| addTitle: 'タイトルを追加...', | addTitle: 'タイトルを追加...', |
| return get(`/explore/apps/${id}`) | return get(`/explore/apps/${id}`) | ||||
| } | } | ||||
| export const fetchInstalledAppList = () => { | |||||
| return get('/installed-apps') | |||||
| export const fetchInstalledAppList = (app_id?: string | null) => { | |||||
| return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`) | |||||
| } | } | ||||
| export const installApp = (id: string) => { | export const installApp = (id: string) => { |