'use client' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import type { Tag } from '@/app/components/base/tag-management/constant' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import type { WorkflowOnlineUser } from '@/models/app' import type { App } from '@/types/app' import * as React from 'react' import { useCallback, useMemo, useState, useTransition } from 'react' import { useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import TagSelector from '@/app/components/base/tag-management/selector' import { AlertDialog, AlertDialogActions, AlertDialogCancelButton, AlertDialogConfirmButton, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, } from '@/app/components/base/ui/alert-dialog' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/app/components/base/ui/dropdown-menu' import { toast } from '@/app/components/base/ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import CornerMark from '@/app/components/plugins/card/base/corner-mark' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' import { useProviderContext } from '@/context/provider-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { AccessMode } from '@/models/access-control' import dynamic from '@/next/dynamic' import { useRouter } from '@/next/navigation' import { useGetUserCanAccessApp } from '@/service/access-control' import { copyApp, exportAppBundle, exportAppConfig, updateAppInfo, upgradeAppRuntime } from '@/service/apps' import { fetchInstalledAppList } from '@/service/explore' import { useDeleteAppMutation } from '@/service/use-apps' import { fetchWorkflowDraft } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { cn } from '@/utils/classnames' import { downloadBlob } from '@/utils/download' import { formatTime } from '@/utils/time' import { basePath } from '@/utils/var' const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { ssr: false, }) const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { ssr: false, }) const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { ssr: false, }) const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false, }) const AccessControl = dynamic(() => import('@/app/components/app/app-access-control'), { ssr: false, }) type AppCardOperationsProps = { app: App open: boolean webappAuthEnabled: boolean isCurrentWorkspaceEditor: boolean exporting: boolean secretEnvListLength: number isUpgradingRuntime: boolean popupClassName: string onOpenChange: (open: boolean) => void onEdit: () => void onDuplicate: () => void onExport: () => void onSwitch: () => void onDelete: () => void onAccessControl: () => void onInstalledApp: () => void onUpgradeRuntime: () => void } const AppCardOperations = ({ app, open, webappAuthEnabled, isCurrentWorkspaceEditor, exporting, secretEnvListLength, isUpgradingRuntime, popupClassName, onOpenChange, onEdit, onDuplicate, onExport, onSwitch, onDelete, onAccessControl, onInstalledApp, onUpgradeRuntime, }: AppCardOperationsProps) => { const { t } = useTranslation() const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: app.id, enabled: !!open && webappAuthEnabled, }) const onClickInstalledApp = async () => { onInstalledApp() } const onClickUpgradeRuntime = async () => { onUpgradeRuntime() } return (
{t('editApp', { ns: 'app' })} {t('duplicate', { ns: 'app' })} 0} className="gap-2 px-3" onClick={onExport}> {t('export', { ns: 'app' })} {(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && ( <> {t('switch', { ns: 'app' })} )} { !app.has_draft_trigger && ( (!webappAuthEnabled) ? ( <> {t('openInExplore', { ns: 'app' })} ) : !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && ( <> {t('openInExplore', { ns: 'app' })} ) ) } { webappAuthEnabled && isCurrentWorkspaceEditor && ( <> {t('accessControl', { ns: 'app' })} ) } {app.runtime_type !== 'sandboxed' && (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) && ( {t('upgradeRuntime', { ns: 'app' })} )} {t('operation.delete', { ns: 'common' })}
) } export type AppCardProps = { app: App onRefresh?: () => void onlineUsers?: WorkflowOnlineUser[] } const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { isCurrentWorkspaceEditor } = useAppContext() const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() const openAsyncWindow = useAsyncWindowOpen() const [showEditModal, setShowEditModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showSwitchModal, setShowSwitchModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [confirmDeleteInput, setConfirmDeleteInput] = useState('') const [showAccessControl, setShowAccessControl] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const [isOperationsOpen, setIsOperationsOpen] = useState(false) const [exporting, startExport] = useTransition() const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() const onConfirmDelete = useCallback(async () => { try { await mutateDeleteApp(app.id) toast.success(t('appDeleted', { ns: 'app' })) onPlanInfoChanged() } catch (e: unknown) { toast.error(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error ? `: ${e.message}` : ''}`) } finally { setShowConfirmDelete(false) setConfirmDeleteInput('') } }, [app.id, mutateDeleteApp, onPlanInfoChanged, t]) const onDeleteDialogOpenChange = useCallback((open: boolean) => { if (isDeleting) return setShowConfirmDelete(open) if (!open) setConfirmDeleteInput('') }, [isDeleting]) const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests, }) => { try { await updateAppInfo({ appID: app.id, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests, }) setShowEditModal(false) toast.success(t('editDone', { ns: 'app' })) if (onRefresh) onRefresh() } catch (e: unknown) { toast.error((e instanceof Error ? e.message : '') || t('editFailed', { ns: 'app' })) } }, [app.id, onRefresh, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { try { const newApp = await copyApp({ appID: app.id, name, icon_type, icon, icon_background, mode: app.mode, }) setShowDuplicateModal(false) toast.success(t('newApp.appCreated', { ns: 'app' })) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (onRefresh) onRefresh() onPlanInfoChanged() getRedirection(isCurrentWorkspaceEditor, newApp, push) } catch { toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } const onExport = async (include = false) => { try { const isDownLoadBundle = app.runtime_type === 'sandboxed' if (isDownLoadBundle) { await exportAppBundle({ appID: app.id, include, }) return } const { data } = await exportAppConfig({ appID: app.id, include, }) const file = new Blob([data], { type: 'application/yaml' }) downloadBlob({ data: file, fileName: `${app.name}.yml` }) } catch { toast.error(t('exportFailed', { ns: 'app' })) } } const exportCheck = async () => { if (app.mode !== AppModeEnum.WORKFLOW && app.mode !== AppModeEnum.ADVANCED_CHAT) { await onExport() return } try { const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`) const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') if (list.length === 0) { await onExport() return } setSecretEnvList(list) } catch { toast.error(t('exportFailed', { ns: 'app' })) } } const [isUpgradingRuntime, startUpgradeRuntime] = useTransition() const onSwitch = () => { if (onRefresh) onRefresh() setShowSwitchModal(false) } const onUpdateAccessControl = useCallback(() => { if (onRefresh) onRefresh() setShowAccessControl(false) }, [onRefresh, setShowAccessControl]) const handleOpenEditModal = useCallback(() => setShowEditModal(true), []) const handleOpenDuplicateModal = useCallback(() => setShowDuplicateModal(true), []) const handleOpenSwitchModal = useCallback(() => setShowSwitchModal(true), []) const handleOpenDeleteModal = useCallback(() => setShowConfirmDelete(true), []) const handleOpenAccessControl = useCallback(() => setShowAccessControl(true), []) const handleExport = useCallback(() => { startExport(async () => { await exportCheck() }) }, [exportCheck, startExport]) const handleInstalledApp = useCallback(async () => { try { await openAsyncWindow(async () => { const { installed_apps } = (await fetchInstalledAppList(app.id) || {}) as { installed_apps?: { id: string }[] } if (installed_apps && installed_apps.length > 0) return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') }, { onError: (err) => { toast.error(`${err.message || err}`) }, }) } catch (e: unknown) { toast.error(e instanceof Error ? e.message : String(e)) } }, [app.id, openAsyncWindow]) const handleUpgradeRuntime = useCallback(() => { startUpgradeRuntime(async () => { try { const res = await upgradeAppRuntime(app.id) if (res.result === 'success' && res.new_app_id) { toast.success(t('sandboxMigrationModal.upgrade', { ns: 'workflow' })) const params = new URLSearchParams({ upgraded_from: app.id, upgraded_from_name: app.name, }) push(`/app/${res.new_app_id}/workflow?${params.toString()}`) } } catch (e: unknown) { toast.error((e instanceof Error ? e.message : '') || 'Upgrade failed') } }) }, [app.id, app.name, push, startUpgradeRuntime, t]) const [tags, setTags] = useState(() => app.tags) const EditTimeText = useMemo(() => { const timeText = formatTime({ date: (app.updated_at || app.created_at) * 1000, dateFormat: `${t('segment.dateTimeFormat', { ns: 'datasetDocuments' })}`, }) return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${timeText}` }, [app.updated_at, app.created_at, t]) const onlineUserAvatars = useMemo(() => { if (!onlineUsers.length) return [] return onlineUsers .map(user => ({ id: user.user_id || user.sid || '', name: user.username || 'User', avatar_url: user.avatar || undefined, })) .filter(user => !!user.id) }, [onlineUsers]) const isSandboxApp = app.runtime_type === 'sandboxed' return ( <>
{isSandboxApp && (
)}
{isCurrentWorkspaceEditor && ( <>
tag.id)} selectedTags={tags} onCacheUpdate={setTags} onChange={onRefresh} />
)}
{showEditModal && ( setShowEditModal(false)} /> )} {showDuplicateModal && ( setShowDuplicateModal(false)} /> )} {showSwitchModal && ( setShowSwitchModal(false)} onSuccess={onSwitch} /> )}
{t('deleteAppConfirmTitle', { ns: 'app' })} {t('deleteAppConfirmContent', { ns: 'app' })}
setConfirmDeleteInput(e.target.value)} />
{t('operation.cancel', { ns: 'common' })} {t('operation.confirm', { ns: 'common' })}
{secretEnvList.length > 0 && ( setSecretEnvList([])} /> )} {showAccessControl && ( setShowAccessControl(false)} /> )} ) } export default React.memo(AppCard)