mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
759 lines
27 KiB
TypeScript
759 lines
27 KiB
TypeScript
'use client'
|
|
|
|
import type {
|
|
AppPartial,
|
|
EnvironmentVariableItemResponse,
|
|
} from '@dify/contracts/api/console/apps/types.gen'
|
|
import type { FormEventHandler, MouseEvent, ReactElement } from 'react'
|
|
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
|
import { zIconType } from '@dify/contracts/api/console/apps/zod.gen'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogActions,
|
|
AlertDialogCancelButton,
|
|
AlertDialogConfirmButton,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogTitle,
|
|
} from '@langgenius/dify-ui/alert-dialog'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuSeparator,
|
|
ContextMenuTrigger,
|
|
} from '@langgenius/dify-ui/context-menu'
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@langgenius/dify-ui/dropdown-menu'
|
|
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
|
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
|
import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group'
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import { Toggle } from '@langgenius/dify-ui/toggle'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
|
import { useMutation, useSuspenseQuery } from '@tanstack/react-query'
|
|
import { useAtomValue } from 'jotai'
|
|
import { useCallback, useMemo, useState } from 'react'
|
|
import { Trans, useTranslation } from 'react-i18next'
|
|
import { useExportAppDsl, useExportWorkflowAppDsl } from '@/app/components/app/use-export-app-dsl'
|
|
import StarIcon from '@/app/components/base/icons/src/vender/Star'
|
|
import { buildInstalledAppPath } from '@/app/components/explore/installed-app/routes'
|
|
import {
|
|
getStepByStepTourDropdownMenuContentProps,
|
|
useStepByStepTourControlledDropdown,
|
|
} from '@/app/components/step-by-step-tour/dropdown-menu'
|
|
import { workspacePermissionKeysAtom } from '@/context/permission-state'
|
|
import { useProviderContext } from '@/context/provider-context'
|
|
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
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/use-app-access-control'
|
|
import { consoleQuery } from '@/service/client'
|
|
import { fetchInstalledAppList } from '@/service/explore'
|
|
import { AppModeEnum } from '@/types/app'
|
|
import { getRedirection } from '@/utils/app-redirection'
|
|
import { getAppACLCapabilities, hasPermission } from '@/utils/permission'
|
|
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 OPERATIONS_MENU_POPUP_CLASS_NAME = 'min-w-[216px]'
|
|
const APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE = new Set<AppPartial['mode']>([
|
|
AppModeEnum.ADVANCED_CHAT,
|
|
AppModeEnum.WORKFLOW,
|
|
])
|
|
|
|
function requiresPublishedWorkflowInExplore(app: AppPartial) {
|
|
return APP_MODES_REQUIRING_PUBLISHED_WORKFLOW_IN_EXPLORE.has(app.mode)
|
|
}
|
|
|
|
type AppCardOperationsMenuItemsProps = {
|
|
app: AppPartial
|
|
kind: 'context' | 'dropdown'
|
|
shouldShowEditOption: boolean
|
|
shouldShowDuplicateOption: boolean
|
|
shouldShowExportOption: boolean
|
|
shouldShowSwitchOption: boolean
|
|
shouldShowAccessConfigOption: boolean
|
|
shouldShowDeleteOption: boolean
|
|
isExporting: boolean
|
|
onEdit: () => void
|
|
onDuplicate: () => void
|
|
onExport: () => void
|
|
onSwitch: () => void
|
|
onDelete: () => void
|
|
onAccessConfig: () => void
|
|
}
|
|
|
|
function AppCardOperationsMenuItems({
|
|
app,
|
|
kind,
|
|
shouldShowEditOption,
|
|
shouldShowDuplicateOption,
|
|
shouldShowExportOption,
|
|
shouldShowSwitchOption,
|
|
shouldShowAccessConfigOption,
|
|
shouldShowDeleteOption,
|
|
isExporting,
|
|
onEdit,
|
|
onDuplicate,
|
|
onExport,
|
|
onSwitch,
|
|
onDelete,
|
|
onAccessConfig,
|
|
}: AppCardOperationsMenuItemsProps) {
|
|
const { t } = useTranslation()
|
|
const openAsyncWindow = useAsyncWindowOpen()
|
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
|
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
|
appId: app.id,
|
|
enabled: systemFeatures.webapp_auth.enabled,
|
|
})
|
|
const needsPublishBeforeExplore = requiresPublishedWorkflowInExplore(app) && !app.workflow?.id
|
|
const shouldShowOpenInExploreOption =
|
|
!app.has_draft_trigger &&
|
|
app.access_mode !== AccessMode.EXTERNAL_MEMBERS &&
|
|
(needsPublishBeforeExplore ||
|
|
!systemFeatures.webapp_auth.enabled ||
|
|
(!isGettingUserCanAccessApp && Boolean(userCanAccessApp?.result)))
|
|
const hasEditGroup = shouldShowEditOption
|
|
const hasCreateExportGroup = shouldShowDuplicateOption || shouldShowExportOption
|
|
const hasSwitchOrExploreGroup = shouldShowSwitchOption || shouldShowOpenInExploreOption
|
|
const hasAccessDeleteGroup = shouldShowAccessConfigOption || shouldShowDeleteOption
|
|
const MenuItem = kind === 'context' ? ContextMenuItem : DropdownMenuItem
|
|
const MenuSeparator = kind === 'context' ? ContextMenuSeparator : DropdownMenuSeparator
|
|
|
|
function handleMenuAction(event: MouseEvent<HTMLElement>, action: () => void) {
|
|
event.stopPropagation()
|
|
event.preventDefault()
|
|
action()
|
|
}
|
|
|
|
async function handleOpenInstalledApp(event: MouseEvent<HTMLElement>) {
|
|
event.stopPropagation()
|
|
event.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: (error) => {
|
|
toast.error(`${error.message || error}`)
|
|
},
|
|
},
|
|
)
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : `${error}`
|
|
toast.error(message)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{shouldShowEditOption && (
|
|
<MenuItem className="gap-2 px-3" onClick={(event) => handleMenuAction(event, onEdit)}>
|
|
<span className="system-sm-regular text-text-secondary">
|
|
{t(($) => $.editApp, { ns: 'app' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
{hasEditGroup &&
|
|
(hasCreateExportGroup || hasSwitchOrExploreGroup || hasAccessDeleteGroup) && (
|
|
<MenuSeparator />
|
|
)}
|
|
{shouldShowDuplicateOption && (
|
|
<MenuItem className="gap-2 px-3" onClick={(event) => handleMenuAction(event, onDuplicate)}>
|
|
<span className="system-sm-regular text-text-secondary">
|
|
{t(($) => $.duplicate, { ns: 'app' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
{shouldShowExportOption && (
|
|
<MenuItem
|
|
className="gap-2 px-3"
|
|
disabled={isExporting}
|
|
onClick={(event) => handleMenuAction(event, onExport)}
|
|
>
|
|
<span className="system-sm-regular text-text-secondary">
|
|
{t(($) => $.export, { ns: 'app' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
{hasCreateExportGroup && (hasSwitchOrExploreGroup || hasAccessDeleteGroup) && (
|
|
<MenuSeparator />
|
|
)}
|
|
{shouldShowSwitchOption && (
|
|
<MenuItem className="gap-2 px-3" onClick={(event) => handleMenuAction(event, onSwitch)}>
|
|
<span className="text-sm/5 text-text-secondary">{t(($) => $.switch, { ns: 'app' })}</span>
|
|
</MenuItem>
|
|
)}
|
|
{shouldShowOpenInExploreOption && (
|
|
<MenuItem className="gap-2 px-3" onClick={handleOpenInstalledApp}>
|
|
<span className="system-sm-regular text-text-secondary">
|
|
{t(($) => $.openInExplore, { ns: 'app' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
{hasSwitchOrExploreGroup && hasAccessDeleteGroup && <MenuSeparator />}
|
|
{shouldShowAccessConfigOption && (
|
|
<MenuItem
|
|
className="gap-2 px-3"
|
|
onClick={(event) => handleMenuAction(event, onAccessConfig)}
|
|
>
|
|
<span className="text-sm/5 text-text-secondary">
|
|
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
{shouldShowAccessConfigOption && shouldShowDeleteOption && <MenuSeparator />}
|
|
{shouldShowDeleteOption && (
|
|
<MenuItem
|
|
variant="destructive"
|
|
className="gap-2 px-3"
|
|
onClick={(event) => handleMenuAction(event, onDelete)}
|
|
>
|
|
<span className="system-sm-regular">
|
|
{t(($) => $['operation.delete'], { ns: 'common' })}
|
|
</span>
|
|
</MenuItem>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
type AppCardInteractionsProps = {
|
|
app: AppPartial
|
|
children: ReactElement
|
|
stepByStepTourActionMenuOpen?: boolean
|
|
stepByStepTourActionMenuHighlightPart?: string
|
|
}
|
|
|
|
export function AppCardInteractions({
|
|
app,
|
|
children,
|
|
stepByStepTourActionMenuOpen = false,
|
|
stepByStepTourActionMenuHighlightPart,
|
|
}: AppCardInteractionsProps) {
|
|
const { t } = useTranslation()
|
|
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
|
const { data: currentUserId } = useSuspenseQuery({
|
|
...userProfileQueryOptions(),
|
|
select: (data) => data.profile.id,
|
|
})
|
|
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
|
const isRbacEnabled = systemFeatures.rbac_enabled
|
|
const { onPlanInfoChanged } = useProviderContext()
|
|
const { push } = useRouter()
|
|
const { mutate: copyApp } = useMutation(consoleQuery.apps.byAppId.copy.post.mutationOptions())
|
|
const { mutateAsync: updateApp } = useMutation(consoleQuery.apps.byAppId.put.mutationOptions())
|
|
const { mutate: deleteApp, isPending: isDeleting } = useMutation(
|
|
consoleQuery.apps.byAppId.delete.mutationOptions(),
|
|
)
|
|
const { mutate: starApp, isPending: isStarring } = useMutation(
|
|
consoleQuery.apps.byAppId.star.post.mutationOptions(),
|
|
)
|
|
const { mutate: unstarApp, isPending: isUnstarring } = useMutation(
|
|
consoleQuery.apps.byAppId.star.delete.mutationOptions(),
|
|
)
|
|
|
|
const [activeDialog, setActiveDialog] = useState<
|
|
'delete' | 'duplicate' | 'edit' | 'switch' | null
|
|
>(null)
|
|
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
|
|
const operationsMenu = useStepByStepTourControlledDropdown({
|
|
allowTriggerCloseWhileControlled: false,
|
|
controlledOpen: stepByStepTourActionMenuOpen,
|
|
})
|
|
const isOperationsMenuOpen = operationsMenu.open
|
|
const setIsOperationsMenuOpen = operationsMenu.onOpenChange
|
|
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariableItemResponse[]>([])
|
|
const { exportAppDsl, isExporting: isAppDslExporting } = useExportAppDsl()
|
|
const { exportWorkflowAppDsl, isExporting: isWorkflowAppDslExporting } = useExportWorkflowAppDsl()
|
|
const isExporting = isAppDslExporting || isWorkflowAppDslExporting
|
|
const isTogglingStar = isStarring || isUnstarring
|
|
const appIconType = zIconType.safeParse(app.icon_type).data ?? null
|
|
const resourceMaintainer = app.maintainer ?? undefined
|
|
const maintainerPermissionOptions = useMemo(
|
|
() => ({
|
|
currentUserId,
|
|
resourceMaintainer,
|
|
workspacePermissionKeys,
|
|
isRbacEnabled,
|
|
}),
|
|
[currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys],
|
|
)
|
|
const appACLCapabilities = useMemo(
|
|
() => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions),
|
|
[app.permission_keys, maintainerPermissionOptions],
|
|
)
|
|
const canCreateApp = hasPermission(workspacePermissionKeys, 'app.create_and_management')
|
|
|
|
const onConfirmDelete = useCallback(() => {
|
|
try {
|
|
deleteApp(
|
|
{ params: { app_id: app.id } },
|
|
{
|
|
onSuccess: () => {
|
|
toast.success(t(($) => $.appDeleted, { ns: 'app' }))
|
|
onPlanInfoChanged()
|
|
setActiveDialog(null)
|
|
setConfirmDeleteInput('')
|
|
},
|
|
onError: (error) => {
|
|
const message = error instanceof Error ? error.message : ''
|
|
toast.error(
|
|
`${t(($) => $.appDeleteFailed, { ns: 'app' })}${message ? `: ${message}` : ''}`,
|
|
)
|
|
},
|
|
},
|
|
)
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : ''
|
|
toast.error(`${t(($) => $.appDeleteFailed, { ns: 'app' })}${message ? `: ${message}` : ''}`)
|
|
}
|
|
}, [app.id, deleteApp, onPlanInfoChanged, t])
|
|
|
|
const onDeleteDialogOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
if (isDeleting) return
|
|
|
|
setActiveDialog(open ? 'delete' : null)
|
|
if (!open) setConfirmDeleteInput('')
|
|
},
|
|
[isDeleting],
|
|
)
|
|
|
|
const deleteNameMismatch = confirmDeleteInput !== app.name
|
|
const isDeleteConfirmDisabled = isDeleting || deleteNameMismatch
|
|
|
|
const onDeleteDialogSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
|
(e) => {
|
|
e.preventDefault()
|
|
if (isDeleteConfirmDisabled) return
|
|
|
|
void onConfirmDelete()
|
|
},
|
|
[isDeleteConfirmDisabled, onConfirmDelete],
|
|
)
|
|
|
|
const handleShowEditModal = useCallback(() => {
|
|
setIsOperationsMenuOpen(false)
|
|
queueMicrotask(() => {
|
|
setActiveDialog('edit')
|
|
})
|
|
}, [setIsOperationsMenuOpen])
|
|
|
|
const handleShowDuplicateModal = useCallback(() => {
|
|
setIsOperationsMenuOpen(false)
|
|
queueMicrotask(() => {
|
|
setActiveDialog('duplicate')
|
|
})
|
|
}, [setIsOperationsMenuOpen])
|
|
|
|
const handleShowSwitchModal = useCallback(() => {
|
|
setIsOperationsMenuOpen(false)
|
|
queueMicrotask(() => {
|
|
setActiveDialog('switch')
|
|
})
|
|
}, [setIsOperationsMenuOpen])
|
|
|
|
const handleShowDeleteConfirm = useCallback(() => {
|
|
setIsOperationsMenuOpen(false)
|
|
queueMicrotask(() => {
|
|
setActiveDialog('delete')
|
|
})
|
|
}, [setIsOperationsMenuOpen])
|
|
|
|
const handleOpenAccessConfig = useCallback(() => {
|
|
setIsOperationsMenuOpen(false)
|
|
push(`/app/${app.id}/access-config`)
|
|
}, [app.id, push, setIsOperationsMenuOpen])
|
|
|
|
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(
|
|
async ({
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
description,
|
|
use_icon_as_answer_icon,
|
|
max_active_requests,
|
|
}) => {
|
|
try {
|
|
await updateApp({
|
|
params: { app_id: app.id },
|
|
body: {
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
description,
|
|
use_icon_as_answer_icon,
|
|
max_active_requests,
|
|
},
|
|
})
|
|
setActiveDialog(null)
|
|
toast.success(t(($) => $.editDone, { ns: 'app' }))
|
|
} catch (e) {
|
|
toast.error(e instanceof Error ? e.message : t(($) => $.editFailed, { ns: 'app' }))
|
|
}
|
|
},
|
|
[app.id, t, updateApp],
|
|
)
|
|
|
|
const onCopy: DuplicateAppModalProps['onConfirm'] = ({
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
}) => {
|
|
try {
|
|
copyApp(
|
|
{
|
|
params: { app_id: app.id },
|
|
body: {
|
|
name,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
},
|
|
},
|
|
{
|
|
onSuccess: (newApp) => {
|
|
if (!('mode' in newApp)) {
|
|
toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
|
return
|
|
}
|
|
|
|
setActiveDialog(null)
|
|
toast.success(t(($) => $['newApp.appCreated'], { ns: 'app' }))
|
|
onPlanInfoChanged()
|
|
getRedirection(newApp, push, {
|
|
currentUserId,
|
|
resourceMaintainer: newApp.maintainer ?? undefined,
|
|
workspacePermissionKeys,
|
|
isRbacEnabled,
|
|
})
|
|
},
|
|
onError: () => toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })),
|
|
},
|
|
)
|
|
} catch {
|
|
toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' }))
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
const onExport = async (include = false) => {
|
|
await exportAppDsl({ appId: app.id, appName: app.name, includeSecret: include })
|
|
}
|
|
|
|
const exportCheck = async () => {
|
|
if (isExporting) return
|
|
|
|
setIsOperationsMenuOpen(false)
|
|
const isWorkflowApp =
|
|
app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT
|
|
const result = isWorkflowApp
|
|
? await exportWorkflowAppDsl({ appId: app.id, appName: app.name })
|
|
: await exportAppDsl({ appId: app.id, appName: app.name })
|
|
if (result?.status === 'confirmation-required') setSecretEnvList(result.secretEnvList)
|
|
}
|
|
|
|
const handleToggleStar = useCallback(
|
|
(pressed: boolean) => {
|
|
if (isTogglingStar) return
|
|
|
|
const mutateStar = pressed ? starApp : unstarApp
|
|
try {
|
|
mutateStar(
|
|
{ params: { app_id: app.id } },
|
|
{
|
|
onError: (error) =>
|
|
toast.error(
|
|
error instanceof Error
|
|
? error.message
|
|
: t(($) => $['studio.starFailed'], { ns: 'app' }),
|
|
),
|
|
},
|
|
)
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : t(($) => $['studio.starFailed'], { ns: 'app' }),
|
|
)
|
|
}
|
|
},
|
|
[app.id, isTogglingStar, starApp, t, unstarApp],
|
|
)
|
|
|
|
const shouldShowEditOption = appACLCapabilities.canEdit
|
|
const shouldShowDuplicateOption = canCreateApp
|
|
const shouldShowExportOption = appACLCapabilities.canImportExportDSL
|
|
const shouldShowSwitchOption =
|
|
appACLCapabilities.canEdit &&
|
|
(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT)
|
|
const shouldShowAccessConfigOption = appACLCapabilities.canAccessConfig
|
|
const shouldShowDeleteOption = appACLCapabilities.canDelete
|
|
const shouldShowOperationsMenu =
|
|
shouldShowEditOption ||
|
|
shouldShowDuplicateOption ||
|
|
shouldShowExportOption ||
|
|
shouldShowSwitchOption ||
|
|
shouldShowAccessConfigOption ||
|
|
shouldShowDeleteOption
|
|
const starToggleLabel = t(($) => $['studio.starApp'], { ns: 'app' })
|
|
const starToggleAccessibleLabel = `${starToggleLabel}: ${app.name}`
|
|
const operationsMenuItemsProps = {
|
|
app,
|
|
shouldShowEditOption,
|
|
shouldShowDuplicateOption,
|
|
shouldShowExportOption,
|
|
shouldShowSwitchOption,
|
|
shouldShowAccessConfigOption,
|
|
shouldShowDeleteOption,
|
|
isExporting,
|
|
onEdit: handleShowEditModal,
|
|
onDuplicate: handleShowDuplicateModal,
|
|
onExport: exportCheck,
|
|
onSwitch: handleShowSwitchModal,
|
|
onDelete: handleShowDeleteConfirm,
|
|
onAccessConfig: handleOpenAccessConfig,
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{shouldShowOperationsMenu ? (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger render={children} />
|
|
<ContextMenuContent className={OPERATIONS_MENU_POPUP_CLASS_NAME}>
|
|
<AppCardOperationsMenuItems kind="context" {...operationsMenuItemsProps} />
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
) : (
|
|
children
|
|
)}
|
|
<div
|
|
className={cn(
|
|
'pointer-events-none absolute top-[-0.5px] right-[-0.5px] flex h-16 w-30 items-start justify-end bg-[linear-gradient(67deg,var(--color-components-card-bg-alt-transparent)_0%,var(--color-components-card-bg-alt)_75%)] p-2 opacity-0',
|
|
isOperationsMenuOpen || isExporting
|
|
? 'opacity-100'
|
|
: 'group-focus-within:opacity-100 group-hover:opacity-100 has-data-popup-open:opacity-100 [@media(hover:none)]:opacity-100',
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-lg backdrop-blur-xs',
|
|
isOperationsMenuOpen || isExporting
|
|
? 'pointer-events-auto'
|
|
: 'pointer-events-none group-focus-within:pointer-events-auto group-hover:pointer-events-auto has-data-popup-open:pointer-events-auto [@media(hover:none)]:pointer-events-auto',
|
|
)}
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger
|
|
render={
|
|
<Toggle
|
|
pressed={app.is_starred}
|
|
disabled={isTogglingStar}
|
|
onPressedChange={handleToggleStar}
|
|
render={
|
|
<IconButton
|
|
size="lg"
|
|
aria-label={starToggleAccessibleLabel}
|
|
className="group disabled:opacity-70"
|
|
>
|
|
<StarIcon
|
|
aria-hidden
|
|
className="size-4.5 text-text-tertiary group-data-pressed:text-text-warning-secondary"
|
|
/>
|
|
</IconButton>
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
<TooltipContent>{starToggleLabel}</TooltipContent>
|
|
</Tooltip>
|
|
{shouldShowOperationsMenu && (
|
|
<DropdownMenu
|
|
modal={false}
|
|
open={isOperationsMenuOpen}
|
|
onOpenChange={setIsOperationsMenuOpen}
|
|
>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<IconButton
|
|
size="lg"
|
|
aria-label={
|
|
isExporting
|
|
? t(($) => $['operation.exporting'], { ns: 'common' })
|
|
: t(($) => $['operation.moreActionsFor'], {
|
|
ns: 'common',
|
|
name: app.name,
|
|
})
|
|
}
|
|
disabled={isExporting}
|
|
className="data-popup-open:bg-state-base-hover"
|
|
>
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
'size-4.5 text-text-tertiary',
|
|
isExporting
|
|
? 'i-ri-loader-2-line animate-spin motion-reduce:animate-none'
|
|
: 'i-ri-more-fill',
|
|
)}
|
|
/>
|
|
</IconButton>
|
|
}
|
|
/>
|
|
<DropdownMenuContent
|
|
placement="bottom-end"
|
|
sideOffset={4}
|
|
{...getStepByStepTourDropdownMenuContentProps({
|
|
highlightPart: stepByStepTourActionMenuHighlightPart,
|
|
interactionMode: operationsMenu.controlled ? 'presentation' : 'interactive',
|
|
className: OPERATIONS_MENU_POPUP_CLASS_NAME,
|
|
})}
|
|
>
|
|
<AppCardOperationsMenuItems kind="dropdown" {...operationsMenuItemsProps} />
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{activeDialog === 'edit' && (
|
|
<EditAppModal
|
|
isEditModal
|
|
appName={app.name}
|
|
appIconType={appIconType}
|
|
appIcon={app.icon ?? ''}
|
|
appIconBackground={app.icon_background}
|
|
appIconUrl={app.icon_url}
|
|
appDescription={app.description ?? ''}
|
|
appMode={app.mode}
|
|
appUseIconAsAnswerIcon={app.use_icon_as_answer_icon ?? false}
|
|
max_active_requests={app.max_active_requests ?? null}
|
|
show
|
|
onConfirm={onEdit}
|
|
onHide={() => setActiveDialog(null)}
|
|
/>
|
|
)}
|
|
{activeDialog === 'duplicate' && (
|
|
<DuplicateAppModal
|
|
appName={app.name}
|
|
icon_type={appIconType}
|
|
icon={app.icon ?? ''}
|
|
icon_background={app.icon_background}
|
|
icon_url={app.icon_url}
|
|
show
|
|
onConfirm={onCopy}
|
|
onHide={() => setActiveDialog(null)}
|
|
/>
|
|
)}
|
|
{activeDialog === 'switch' && (
|
|
<SwitchAppModal show appDetail={app} onClose={() => setActiveDialog(null)} />
|
|
)}
|
|
<AlertDialog open={activeDialog === 'delete'} onOpenChange={onDeleteDialogOpenChange}>
|
|
<AlertDialogContent>
|
|
<form className="flex flex-col" onSubmit={onDeleteDialogSubmit}>
|
|
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
|
|
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
|
|
{t(($) => $.deleteAppConfirmTitle, { ns: 'app' })}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
|
|
{t(($) => $.deleteAppConfirmContent, { ns: 'app' })}
|
|
</AlertDialogDescription>
|
|
<Field name="confirm-app-name" className="mt-2">
|
|
<FieldLabel className="mb-1 block py-0 system-sm-regular text-text-secondary">
|
|
<Trans
|
|
i18nKey={($) => $.deleteAppConfirmInputLabel}
|
|
ns="app"
|
|
values={{ appName: app.name }}
|
|
components={{
|
|
appName: (
|
|
<span className="system-sm-semibold text-text-primary" translate="no" />
|
|
),
|
|
}}
|
|
/>
|
|
</FieldLabel>
|
|
<InputGroup className="border-components-input-border-hover">
|
|
<InputGroupInput
|
|
type="text"
|
|
autoComplete="off"
|
|
spellCheck={false}
|
|
placeholder={t(($) => $.deleteAppConfirmInputPlaceholder, { ns: 'app' })}
|
|
value={confirmDeleteInput}
|
|
onValueChange={setConfirmDeleteInput}
|
|
/>
|
|
<InputGroupAddon align="inline-end" className="min-w-20 justify-end pe-1.75">
|
|
<Button
|
|
variant="tertiary"
|
|
size="small"
|
|
onClick={() => setConfirmDeleteInput(app.name)}
|
|
className="rounded-full px-2.5"
|
|
>
|
|
{t(($) => $['operation.fill'], { ns: 'common' })}
|
|
</Button>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</Field>
|
|
</div>
|
|
<AlertDialogActions>
|
|
<AlertDialogCancelButton type="button" disabled={isDeleting}>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</AlertDialogCancelButton>
|
|
<AlertDialogConfirmButton
|
|
type="submit"
|
|
loading={isDeleting}
|
|
disabled={deleteNameMismatch}
|
|
>
|
|
{t(($) => $['operation.confirm'], { ns: 'common' })}
|
|
</AlertDialogConfirmButton>
|
|
</AlertDialogActions>
|
|
</form>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
{secretEnvList.length > 0 && (
|
|
<DSLExportConfirmModal
|
|
envList={secretEnvList}
|
|
onConfirm={onExport}
|
|
onClose={() => setSecretEnvList([])}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|