'use client' import type { FormEvent, FormEventHandler, KeyboardEvent, MouseEvent } from 'react' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' 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 { AlertDialog, AlertDialogActions, AlertDialogCancelButton, AlertDialogConfirmButton, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useId, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import { useSetNeedRefreshAppList } from '@/app/components/apps/storage' import AppIcon from '@/app/components/base/app-icon' import StarIcon from '@/app/components/base/icons/src/vender/Star' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { buildInstalledAppPath } from '@/app/components/explore/installed-app/routes' import { getStepByStepTourDropdownMenuContentProps, useStepByStepTourControlledDropdown, } from '@/app/components/step-by-step-tour/dropdown-menu' import { userProfileIdAtom } from '@/context/account-state' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AppCardTags } from '@/features/tag-management/components/app-card-tags' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { AccessMode } from '@/models/access-control' import dynamic from '@/next/dynamic' import Link from '@/next/link' import { useRouter } from '@/next/navigation' import { useGetUserCanAccessApp } from '@/service/access-control/use-app-access-control' import { copyApp, exportAppConfig, updateAppInfo } from '@/service/apps' import { fetchInstalledAppList } from '@/service/explore' import { useDeleteAppMutation, useToggleAppStarMutation } from '@/service/use-apps' import { fetchWorkflowDraft } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import { getRedirection, getRedirectionPath } from '@/utils/app-redirection' import { downloadBlob } from '@/utils/download' import { getAppACLCapabilities, hasOnlyAppPreviewPermission, hasPermission, } from '@/utils/permission' 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, }) const ACCESS_MODE_ICON_CLASS_NAMES: Record = { [AccessMode.PUBLIC]: 'i-ri-global-line', [AccessMode.SPECIFIC_GROUPS_MEMBERS]: 'i-ri-lock-line', [AccessMode.ORGANIZATION]: 'i-ri-building-line', [AccessMode.EXTERNAL_MEMBERS]: 'i-ri-verified-badge-line', } const ACCESS_MODE_LABEL_KEYS = { [AccessMode.PUBLIC]: 'accessItemsDescription.anyone', [AccessMode.SPECIFIC_GROUPS_MEMBERS]: 'accessItemsDescription.specific', [AccessMode.ORGANIZATION]: 'accessItemsDescription.organization', [AccessMode.EXTERNAL_MEMBERS]: 'accessItemsDescription.external', } as const const APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE = new Set([ AppModeEnum.ADVANCED_CHAT, AppModeEnum.WORKFLOW, ]) type AppCardProps = { app: App onlineUsers?: WorkflowOnlineUser[] onRefresh?: () => void onOpenTagManagement?: () => void stepByStepTourActionMenuOpen?: boolean stepByStepTourCardTarget?: string stepByStepTourCardHighlightPart?: string stepByStepTourActionMenuHighlightPart?: string } type AppAccessModeIconProps = { accessMode?: AccessMode | null } const getAppResourceMaintainer = (app: App) => app.maintainer function requiresPublishedWorkflowInExplore(app: App) { return APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE.has(app.mode) } function AppAccessModeIcon({ accessMode }: AppAccessModeIconProps) { const { t } = useTranslation() if (!accessMode) return null const iconClassName = ACCESS_MODE_ICON_CLASS_NAMES[accessMode] const labelKey = ACCESS_MODE_LABEL_KEYS[accessMode] if (!iconClassName || !labelKey) return null const label = t(($) => $[labelKey], { ns: 'app' }) return (
} /> {label}
) } type AppCardOperationsMenuProps = { app: App shouldShowEditOption: boolean shouldShowDuplicateOption: boolean shouldShowExportOption: boolean shouldShowSwitchOption: boolean shouldShowOpenInExploreOption: boolean shouldShowAccessControlOption: boolean shouldShowAccessConfigOption: boolean shouldShowDeleteOption: boolean onEdit: () => void onDuplicate: () => void onExport: () => void onSwitch: () => void onDelete: () => void onAccessControl: () => void onAccessConfig: () => void } function AppCardOperationsMenu({ app, shouldShowEditOption, shouldShowDuplicateOption, shouldShowExportOption, shouldShowSwitchOption, shouldShowOpenInExploreOption, shouldShowAccessControlOption, shouldShowAccessConfigOption, shouldShowDeleteOption, onEdit, onDuplicate, onExport, onSwitch, onDelete, onAccessControl, onAccessConfig, }: AppCardOperationsMenuProps) { const { t } = useTranslation() const openAsyncWindow = useAsyncWindowOpen() const hasEditGroup = shouldShowEditOption const hasCreateExportGroup = shouldShowDuplicateOption || shouldShowExportOption const hasSwitchOrExploreGroup = shouldShowSwitchOption || shouldShowOpenInExploreOption const hasAccessDeleteGroup = shouldShowAccessControlOption || shouldShowAccessConfigOption || shouldShowDeleteOption function handleMenuAction(e: MouseEvent, action: () => void) { e.stopPropagation() e.preventDefault() action() } async function handleOpenInstalledApp(e: MouseEvent) { e.stopPropagation() e.preventDefault() if (requiresPublishedWorkflowInExplore(app) && !app.workflow?.id) { toast.error(t(($) => $.notPublishedYet, { ns: 'app' })) return } try { await openAsyncWindow( async () => { const { installed_apps } = await fetchInstalledAppList(app.id) if (installed_apps?.length > 0) return `${basePath}${buildInstalledAppPath(installed_apps[0]!.id)}` throw new Error(t(($) => $.notPublishedYet, { ns: 'app' })) }, { onError: (err) => { toast.error(`${err.message || err}`) }, }, ) } catch (e: unknown) { const message = e instanceof Error ? e.message : `${e}` toast.error(message) } } return ( <> {shouldShowEditOption && ( handleMenuAction(e, onEdit)}> {t(($) => $.editApp, { ns: 'app' })} )} {hasEditGroup && (hasCreateExportGroup || hasSwitchOrExploreGroup || hasAccessDeleteGroup) && ( )} {shouldShowDuplicateOption && ( handleMenuAction(e, onDuplicate)}> {t(($) => $.duplicate, { ns: 'app' })} )} {shouldShowExportOption && ( handleMenuAction(e, onExport)}> {t(($) => $.export, { ns: 'app' })} )} {hasCreateExportGroup && (hasSwitchOrExploreGroup || hasAccessDeleteGroup) && ( )} {shouldShowSwitchOption && ( handleMenuAction(e, onSwitch)}> {t(($) => $.switch, { ns: 'app' })} )} {shouldShowOpenInExploreOption && ( {t(($) => $.openInExplore, { ns: 'app' })} )} {hasSwitchOrExploreGroup && hasAccessDeleteGroup && } {shouldShowAccessControlOption && ( handleMenuAction(e, onAccessControl)} > {t(($) => $.accessControl, { ns: 'app' })} )} {shouldShowAccessConfigOption && ( handleMenuAction(e, onAccessConfig)} > {t(($) => $['settings.resourceAccess'], { ns: 'common' })} )} {(shouldShowAccessControlOption || shouldShowAccessConfigOption) && shouldShowDeleteOption && } {shouldShowDeleteOption && ( handleMenuAction(e, onDelete)} > {t(($) => $['operation.delete'], { ns: 'common' })} )} ) } type AppCardOperationsMenuContentProps = Omit< AppCardOperationsMenuProps, 'shouldShowOpenInExploreOption' > function AppCardOperationsMenuContent(props: AppCardOperationsMenuContentProps) { const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: props.app.id, enabled: systemFeatures.webapp_auth.enabled, }) const needsPublishBeforeExplore = requiresPublishedWorkflowInExplore(props.app) && !props.app.workflow?.id const shouldShowOpenInExploreOption = !props.app.has_draft_trigger && (needsPublishBeforeExplore || !systemFeatures.webapp_auth.enabled || (!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result))) return ( ) } type AppCardActionBarProps = { app: App onRefresh?: () => void } export function AppCardActionBar({ app, onRefresh }: AppCardActionBarProps) { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const currentUserId = useAtomValue(userProfileIdAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const isRbacEnabled = systemFeatures.rbac_enabled const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() 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 [isOperationsMenuOpen, setIsOperationsMenuOpen] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation() const setNeedRefresh = useSetNeedRefreshAppList() const resourceMaintainer = getAppResourceMaintainer(app) const maintainerPermissionOptions = useMemo( () => ({ currentUserId, resourceMaintainer, workspacePermissionKeys, isRbacEnabled, }), [currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys], ) const appACLCapabilities = useMemo( () => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions), [app.permission_keys, maintainerPermissionOptions], ) const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const onConfirmDelete = useCallback(async () => { try { await mutateDeleteApp(app.id) toast.success(t(($) => $.appDeleted, { ns: 'app' })) onPlanInfoChanged() setShowConfirmDelete(false) setConfirmDeleteInput('') } catch (e) { const message = e instanceof Error ? e.message : '' toast.error(`${t(($) => $.appDeleteFailed, { ns: 'app' })}${message ? `: ${message}` : ''}`) } }, [app.id, mutateDeleteApp, onPlanInfoChanged, t]) const onDeleteDialogOpenChange = useCallback( (open: boolean) => { if (isDeleting) return setShowConfirmDelete(open) if (!open) setConfirmDeleteInput('') }, [isDeleting], ) const isDeleteConfirmDisabled = isDeleting || confirmDeleteInput !== app.name const onDeleteDialogSubmit: FormEventHandler = useCallback( (e) => { e.preventDefault() if (isDeleteConfirmDisabled) return void onConfirmDelete() }, [isDeleteConfirmDisabled, onConfirmDelete], ) const handleShowEditModal = useCallback(() => { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowEditModal(true) }) }, []) const handleShowDuplicateModal = useCallback(() => { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowDuplicateModal(true) }) }, []) const handleShowSwitchModal = useCallback(() => { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowSwitchModal(true) }) }, []) const handleShowDeleteConfirm = useCallback(() => { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowConfirmDelete(true) }) }, []) const handleShowAccessControl = useCallback(() => { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowAccessControl(true) }) }, []) const handleOpenAccessConfig = useCallback(() => { setIsOperationsMenuOpen(false) push(`/app/${app.id}/access-config`) }, [app.id, push]) 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' })) onRefresh?.() } catch (e) { 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' })) setNeedRefresh('1') onRefresh?.() onPlanInfoChanged() getRedirection(newApp, push, { currentUserId, resourceMaintainer: getAppResourceMaintainer(newApp), workspacePermissionKeys, isRbacEnabled, }) } catch { toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) } } const onExport = async (include = false) => { try { 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 () => { setIsOperationsMenuOpen(false) if (app.mode !== AppModeEnum.WORKFLOW && app.mode !== AppModeEnum.ADVANCED_CHAT) { 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) { onExport() return } setSecretEnvList(list) } catch { toast.error(t(($) => $.exportFailed, { ns: 'app' })) } } const onSwitch = () => { onRefresh?.() setShowSwitchModal(false) } const onUpdateAccessControl = useCallback(() => { onRefresh?.() setShowAccessControl(false) }, [onRefresh, setShowAccessControl]) const handleToggleStar = useCallback( async (e: MouseEvent) => { e.stopPropagation() e.preventDefault() if (isTogglingStar) return try { await mutateToggleAppStar({ appId: app.id, isStarred: Boolean(app.is_starred), }) onRefresh?.() } catch (error) { toast.error( error instanceof Error ? error.message : t(($) => $['studio.starFailed'], { ns: 'app' }), ) } }, [app.id, app.is_starred, isTogglingStar, mutateToggleAppStar, onRefresh, t], ) const shouldShowEditOption = appACLCapabilities.canEdit const shouldShowDuplicateOption = canCreateApp const shouldShowExportOption = appACLCapabilities.canImportExportDSL const shouldShowSwitchOption = canCreateApp && appACLCapabilities.canEdit && (app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) const shouldShowAccessControlOption = systemFeatures.webapp_auth.enabled && appACLCapabilities.canReleaseAndVersion const shouldShowAccessConfigOption = appACLCapabilities.canAccessConfig const shouldShowDeleteOption = appACLCapabilities.canDelete const shouldShowOperationsMenu = shouldShowEditOption || shouldShowDuplicateOption || shouldShowExportOption || shouldShowSwitchOption || shouldShowAccessControlOption || shouldShowAccessConfigOption || shouldShowDeleteOption const operationsMenuWidthClassName = shouldShowSwitchOption ? 'w-[256px]' : 'w-[216px]' const starActionLabel = app.is_starred ? t(($) => $['studio.unstarApp'], { ns: 'app' }) : t(($) => $['studio.starApp'], { ns: 'app' }) return ( <> {!isPreviewOnly && (
} /> {starActionLabel} {shouldShowOperationsMenu && ( $['operation.moreActionsFor'], { ns: 'common', name: app.name, })} className={cn( 'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden', isOperationsMenuOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover', )} onClick={(e) => { e.stopPropagation() e.preventDefault() }} > {t(($) => $['operation.more'], { ns: 'common' })} {systemFeatures.webapp_auth.enabled ? ( ) : ( )} )}
)} {showEditModal && ( setShowEditModal(false)} /> )} {showDuplicateModal && ( setShowDuplicateModal(false)} /> )} {showSwitchModal && ( setShowSwitchModal(false)} onSuccess={onSwitch} /> )}
{t(($) => $.deleteAppConfirmTitle, { ns: 'app' })} {t(($) => $.deleteAppConfirmContent, { ns: 'app' })} $.deleteAppConfirmInputLabel} ns="app" values={{ appName: app.name }} components={{ appName: ( ), }} /> $.deleteAppConfirmInputPlaceholder, { ns: 'app' })} value={confirmDeleteInput} onValueChange={setConfirmDeleteInput} className="border-components-input-border-hover bg-components-input-bg-normal focus:border-components-input-border-active focus:bg-components-input-bg-active" />
{t(($) => $['operation.cancel'], { ns: 'common' })} {t(($) => $['operation.confirm'], { ns: 'common' })}
{secretEnvList.length > 0 && ( setSecretEnvList([])} /> )} {showAccessControl && ( setShowAccessControl(false)} /> )} ) } export function AppCard({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () => {}, stepByStepTourActionMenuOpen = false, stepByStepTourCardTarget, stepByStepTourCardHighlightPart, stepByStepTourActionMenuHighlightPart, }: AppCardProps) { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const currentUserId = useAtomValue(userProfileIdAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const isRbacEnabled = systemFeatures.rbac_enabled const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() 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 operationsMenu = useStepByStepTourControlledDropdown({ allowTriggerCloseWhileControlled: false, controlledOpen: stepByStepTourActionMenuOpen, }) const isOperationsMenuOpen = operationsMenu.open const setIsOperationsMenuOpen = operationsMenu.onOpenChange const [secretEnvList, setSecretEnvList] = useState([]) const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation() const { mutateAsync: mutateToggleAppStar, isPending: isTogglingStar } = useToggleAppStarMutation() const setNeedRefresh = useSetNeedRefreshAppList() const resourceMaintainer = getAppResourceMaintainer(app) const maintainerPermissionOptions = useMemo( () => ({ currentUserId, resourceMaintainer, workspacePermissionKeys, isRbacEnabled, }), [currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys], ) const appACLCapabilities = useMemo( () => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions), [app.permission_keys, maintainerPermissionOptions], ) const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management') const canManageAppTags = hasPermission(workspacePermissionKeys, 'app.tag.manage') async function onConfirmDelete() { try { await mutateDeleteApp(app.id) toast.success(t(($) => $.appDeleted, { ns: 'app' })) onPlanInfoChanged() setShowConfirmDelete(false) setConfirmDeleteInput('') } catch (e) { const message = e instanceof Error ? e.message : '' toast.error(`${t(($) => $.appDeleteFailed, { ns: 'app' })}${message ? `: ${message}` : ''}`) } } function onDeleteDialogOpenChange(open: boolean) { if (isDeleting) return setShowConfirmDelete(open) if (!open) setConfirmDeleteInput('') } const isDeleteConfirmDisabled = isDeleting || confirmDeleteInput !== app.name function onDeleteDialogSubmit(event: FormEvent) { event.preventDefault() if (isDeleteConfirmDisabled) return void onConfirmDelete() } function handleShowEditModal() { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowEditModal(true) }) } function handleShowDuplicateModal() { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowDuplicateModal(true) }) } function handleShowSwitchModal() { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowSwitchModal(true) }) } function handleShowDeleteConfirm() { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowConfirmDelete(true) }) } function handleShowAccessControl() { setIsOperationsMenuOpen(false) queueMicrotask(() => { setShowAccessControl(true) }) } const handleOpenAccessConfig = useCallback(() => { setIsOperationsMenuOpen(false) push(`/app/${app.id}/access-config`) }, [app.id, push]) const onEdit: CreateAppModalProps['onConfirm'] = 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) { toast.error(e instanceof Error ? e.message : t(($) => $.editFailed, { ns: 'app' })) } } 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' })) setNeedRefresh('1') if (onRefresh) onRefresh() onPlanInfoChanged() getRedirection(newApp, push, { currentUserId, resourceMaintainer: getAppResourceMaintainer(newApp), workspacePermissionKeys, isRbacEnabled, }) } catch { toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) } } const onExport = async (include = false) => { try { 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 () => { setIsOperationsMenuOpen(false) if (app.mode !== AppModeEnum.WORKFLOW && app.mode !== AppModeEnum.ADVANCED_CHAT) { 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) { onExport() return } setSecretEnvList(list) } catch { toast.error(t(($) => $.exportFailed, { ns: 'app' })) } } const onSwitch = () => { if (onRefresh) onRefresh() setShowSwitchModal(false) } function onUpdateAccessControl() { if (onRefresh) onRefresh() setShowAccessControl(false) } const handleToggleStar = useCallback( async (e: React.MouseEvent) => { e.stopPropagation() e.preventDefault() if (isTogglingStar) return try { await mutateToggleAppStar({ appId: app.id, isStarred: Boolean(app.is_starred), }) onRefresh?.() } catch (error) { toast.error( error instanceof Error ? error.message : t(($) => $['studio.starFailed'], { ns: 'app' }), ) } }, [app.id, app.is_starred, isTogglingStar, mutateToggleAppStar, onRefresh, t], ) const shouldShowEditOption = appACLCapabilities.canEdit const shouldShowDuplicateOption = canCreateApp const shouldShowExportOption = appACLCapabilities.canImportExportDSL const shouldShowSwitchOption = appACLCapabilities.canEdit && (app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) const shouldShowAccessControlOption = systemFeatures.webapp_auth.enabled && appACLCapabilities.canReleaseAndVersion const shouldShowAccessConfigOption = appACLCapabilities.canAccessConfig const shouldShowDeleteOption = appACLCapabilities.canDelete const shouldShowOperationsMenu = shouldShowEditOption || shouldShowDuplicateOption || shouldShowExportOption || shouldShowSwitchOption || shouldShowAccessControlOption || shouldShowAccessConfigOption || shouldShowDeleteOption const canBindOrUnbindTags = !isPreviewOnly && (canManageAppTags || appACLCapabilities.canEdit) const operationsMenuWidthClassName = shouldShowSwitchOption ? 'w-[256px]' : 'w-[216px]' 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 appModeLabel = useMemo(() => { switch (app.mode) { case AppModeEnum.CHAT: return t(($) => $['types.chatbot'], { ns: 'app' }) case AppModeEnum.ADVANCED_CHAT: return t(($) => $['types.advanced'], { ns: 'app' }) case AppModeEnum.AGENT_CHAT: return t(($) => $['types.agent'], { ns: 'app' }) case AppModeEnum.COMPLETION: return t(($) => $['types.completion'], { ns: 'app' }) case AppModeEnum.WORKFLOW: return t(($) => $['types.workflow'], { ns: 'app' }) default: return app.mode } }, [app.mode, t]) const onlinePresenceUsers = useMemo(() => { return onlineUsers .map((user, index) => { const id = user.user_id || user.sid || `${app.id}-online-${index}` const name = user.username || user.user_id || user.sid || `${index + 1}` return { id, name, avatar_url: user.avatar || null, } }) .filter((user) => Boolean(user.id)) }, [app.id, onlineUsers]) const appNameId = useId() const appDescriptionId = useId() const appHref = getRedirectionPath(app, maintainerPermissionOptions) const appCardClassName = cn( 'inline-flex h-full w-full touch-manipulation flex-col overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs outline-hidden transition-shadow duration-200 ease-in-out', isPreviewOnly ? 'cursor-not-allowed opacity-60 focus-visible:ring-2 focus-visible:ring-state-accent-solid' : 'cursor-pointer hover:shadow-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid', ) const starActionLabel = app.is_starred ? t(($) => $['studio.unstarApp'], { ns: 'app' }) : t(($) => $['studio.starApp'], { ns: 'app' }) const showPreviewOnlyAccessWarning = useCallback(() => { toast.warning(t(($) => $.noAccessResourcePermission, { ns: 'app' })) }, [t]) const handlePreviewOnlyCardKeyDown = useCallback( (event: KeyboardEvent) => { if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() showPreviewOnlyAccessWarning() }, [showPreviewOnlyAccessWarning], ) const appCardContent = ( <>
{app.name}
{appModeLabel}
{onlinePresenceUsers.length > 0 && (
)}
{app.description}
{app.author_name && ( <>
{app.author_name}
ยท
)}
{editTimeText}
) return ( <>
{isPreviewOnly ? (
{appCardContent}
) : ( {appCardContent} )}
{ e.stopPropagation() e.preventDefault() }} >
{!isPreviewOnly && (
} /> {starActionLabel} {shouldShowOperationsMenu && ( $['operation.moreActionsFor'], { ns: 'common', name: app.name, })} className={cn( 'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden', isOperationsMenuOpen ? 'bg-state-base-hover' : 'hover:bg-state-base-hover', )} onClick={(e) => { e.stopPropagation() e.preventDefault() }} > {t(($) => $['operation.more'], { ns: 'common' })} {systemFeatures.webapp_auth.enabled ? ( ) : ( )} )}
)}
{showEditModal && ( setShowEditModal(false)} /> )} {showDuplicateModal && ( setShowDuplicateModal(false)} /> )} {showSwitchModal && ( setShowSwitchModal(false)} onSuccess={onSwitch} /> )}
{t(($) => $.deleteAppConfirmTitle, { ns: 'app' })} {t(($) => $.deleteAppConfirmContent, { ns: 'app' })} $.deleteAppConfirmInputLabel} ns="app" values={{ appName: app.name }} components={{ appName: ( ), }} />
$.deleteAppConfirmInputPlaceholder, { ns: 'app' })} value={confirmDeleteInput} onValueChange={setConfirmDeleteInput} className="border-components-input-border-hover bg-components-input-bg-normal pr-20 focus:border-components-input-border-active focus:bg-components-input-bg-active" />
{t(($) => $['operation.cancel'], { ns: 'common' })} {t(($) => $['operation.confirm'], { ns: 'common' })}
{secretEnvList.length > 0 && ( setSecretEnvList([])} /> )} {showAccessControl && ( setShowAccessControl(false)} /> )} ) }