mirror of
https://github.com/langgenius/dify.git
synced 2026-03-27 17:19:55 +08:00
refactor(app-card): replace Popover with DropdownMenu for improved UI interactions
This commit is contained in:
@ -11,7 +11,6 @@ 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 Divider from '@/app/components/base/divider'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import {
|
||||
AlertDialog,
|
||||
@ -23,16 +22,14 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/app/components/base/ui/alert-dialog'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/app/components/base/ui/popover'
|
||||
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 { Tooltip, TooltipContent, TooltipTrigger } from '@/app/components/base/ui/tooltip'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -72,12 +69,14 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont
|
||||
|
||||
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
|
||||
@ -90,12 +89,14 @@ type AppCardOperationsProps = {
|
||||
|
||||
const AppCardOperations = ({
|
||||
app,
|
||||
open,
|
||||
webappAuthEnabled,
|
||||
isCurrentWorkspaceEditor,
|
||||
exporting,
|
||||
secretEnvListLength,
|
||||
isUpgradingRuntime,
|
||||
popupClassName,
|
||||
onOpenChange,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onExport,
|
||||
@ -106,67 +107,56 @@ const AppCardOperations = ({
|
||||
onUpgradeRuntime,
|
||||
}: AppCardOperationsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({
|
||||
appId: app.id,
|
||||
enabled: !!open && webappAuthEnabled,
|
||||
})
|
||||
|
||||
const onMouseLeave = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClickInstalledApp = async () => {
|
||||
onInstalledApp()
|
||||
onMouseLeave()
|
||||
}
|
||||
|
||||
const onClickUpgradeRuntime = async () => {
|
||||
onUpgradeRuntime()
|
||||
onMouseLeave()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md',
|
||||
open && '!bg-state-base-hover !shadow-none',
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">{t('operation.more', { ns: 'common' })}</span>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
<DropdownMenu open={open} onOpenChange={onOpenChange}>
|
||||
<DropdownMenuTrigger
|
||||
type="button"
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
|
||||
open && 'bg-state-base-hover shadow-none',
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
>
|
||||
<span aria-hidden className="i-ri-more-fill h-4 w-4 text-text-tertiary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName={cn(
|
||||
'w-fit min-w-[130px] overflow-hidden rounded-lg bg-components-panel-bg shadow-lg ring-1 ring-black/5',
|
||||
'w-fit min-w-[130px]',
|
||||
popupClassName,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onEdit}>
|
||||
<div className="flex w-full flex-col">
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={onEdit}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('editApp', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onDuplicate}>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={onDuplicate}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('duplicate', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<button type="button" disabled={exporting || secretEnvListLength > 0} className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50" onClick={onExport}>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled={exporting || secretEnvListLength > 0} className="gap-2 px-3" onClick={onExport}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('export', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
{(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onSwitch}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="px-3" onClick={onSwitch}>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('switch', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
@ -174,59 +164,54 @@ const AppCardOperations = ({
|
||||
(!webappAuthEnabled)
|
||||
? (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)
|
||||
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
|
||||
<>
|
||||
<Divider className="my-1" />
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover" onClick={onClickInstalledApp}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={onClickInstalledApp}>
|
||||
<span className="text-text-secondary system-sm-regular">{t('openInExplore', { ns: 'app' })}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
<Divider className="my-1" />
|
||||
<DropdownMenuSeparator />
|
||||
{
|
||||
webappAuthEnabled && isCurrentWorkspaceEditor && (
|
||||
<>
|
||||
<button type="button" className="mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover" onClick={onAccessControl}>
|
||||
<DropdownMenuItem className="px-3" onClick={onAccessControl}>
|
||||
<span className="text-sm leading-5 text-text-secondary">{t('accessControl', { ns: 'app' })}</span>
|
||||
</button>
|
||||
<Divider className="my-1" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{app.runtime_type !== 'sandboxed'
|
||||
&& (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
&& (
|
||||
<button
|
||||
type="button"
|
||||
<DropdownMenuItem
|
||||
disabled={isUpgradingRuntime}
|
||||
className="mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="gap-2 px-3"
|
||||
onClick={onClickUpgradeRuntime}
|
||||
>
|
||||
<span className="text-text-accent system-sm-regular">
|
||||
{t('upgradeRuntime', { ns: 'app' })}
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<span className="text-text-secondary system-sm-regular group-hover:text-text-destructive">
|
||||
<DropdownMenuItem className="group gap-2 px-3 py-[6px] data-[highlighted]:bg-state-destructive-hover" destructive onClick={onDelete}>
|
||||
<span className="system-sm-regular">
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@ -251,6 +236,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
|
||||
const [showAccessControl, setShowAccessControl] = useState(false)
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const [isOperationsOpen, setIsOperationsOpen] = useState(false)
|
||||
const [exporting, startExport] = useTransition()
|
||||
const { mutateAsync: mutateDeleteApp, isPending: isDeleting } = useDeleteAppMutation()
|
||||
|
||||
@ -458,84 +444,81 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
getRedirection(isCurrentWorkspaceEditor, app, push)
|
||||
}}
|
||||
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
|
||||
className="group relative col-span-1 inline-flex h-[160px] flex-col rounded-xl border-[1px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
|
||||
>
|
||||
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]">
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={app.icon_type}
|
||||
icon={app.icon}
|
||||
background={app.icon_background}
|
||||
imageUrl={app.icon_url}
|
||||
/>
|
||||
<AppTypeIcon type={app.mode} wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm" className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="w-0 grow py-[1px]">
|
||||
<div className="flex items-center text-sm font-semibold leading-5 text-text-secondary">
|
||||
<div className="truncate" title={app.name}>{app.name}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => getRedirection(isCurrentWorkspaceEditor, app, push)}
|
||||
className={cn(
|
||||
'flex min-h-0 flex-1 flex-col text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
|
||||
isCurrentWorkspaceEditor && 'pb-[42px]',
|
||||
)}
|
||||
>
|
||||
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]">
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={app.icon_type}
|
||||
icon={app.icon}
|
||||
background={app.icon_background}
|
||||
imageUrl={app.icon_url}
|
||||
/>
|
||||
<AppTypeIcon type={app.mode} wrapperClassName="absolute -bottom-0.5 -right-0.5 w-4 h-4 shadow-sm" className="h-3 w-3" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
|
||||
<div className="truncate" title={app.author_name}>{app.author_name}</div>
|
||||
<div>·</div>
|
||||
<div className="truncate" title={EditTimeText}>{EditTimeText}</div>
|
||||
<div className="w-0 grow py-[1px]">
|
||||
<div className="flex items-center text-sm font-semibold leading-5 text-text-secondary">
|
||||
<div className="truncate" title={app.name}>{app.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
|
||||
<div className="truncate" title={app.author_name}>{app.author_name}</div>
|
||||
<div>·</div>
|
||||
<div className="truncate" title={EditTimeText}>{EditTimeText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
{app.access_mode === AccessMode.PUBLIC && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-global-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.anyone', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-lock-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.specific', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.ORGANIZATION && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-building-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.organization', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.external', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{onlineUserAvatars.length > 0 ? <UserAvatarList users={onlineUserAvatars} maxVisible={3} size={20} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
{app.access_mode === AccessMode.PUBLIC && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-global-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.anyone', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-lock-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.specific', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.ORGANIZATION && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-building-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.organization', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{app.access_mode === AccessMode.EXTERNAL_MEMBERS && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span className="i-ri-verified-badge-line h-4 w-4 text-text-quaternary" />} />
|
||||
<TooltipContent>{t('accessItemsDescription.external', { ns: 'app' })}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div className="min-h-0 flex-1 px-[14px] text-xs leading-normal text-text-tertiary">
|
||||
<div
|
||||
className="line-clamp-2"
|
||||
title={app.description}
|
||||
>
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{onlineUserAvatars.length > 0 && (
|
||||
<UserAvatarList users={onlineUserAvatars} maxVisible={3} size={20} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[90px] px-[14px] text-xs leading-normal text-text-tertiary">
|
||||
<div
|
||||
className="line-clamp-2"
|
||||
title={app.description}
|
||||
>
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="absolute bottom-1 left-0 right-0 flex h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1">
|
||||
{isCurrentWorkspaceEditor && (
|
||||
<>
|
||||
<div
|
||||
className={cn('flex w-0 grow items-center gap-1')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="mr-[41px] w-full grow group-hover:!mr-0">
|
||||
<div className={cn('flex w-0 grow items-center gap-1')}>
|
||||
<div className={cn('mr-[41px] w-full grow group-hover:!mr-0', isOperationsOpen && '!mr-0')}>
|
||||
<TagSelector
|
||||
position="bl"
|
||||
type="app"
|
||||
@ -547,10 +530,21 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex" />
|
||||
<div className="!hidden shrink-0 group-hover:!flex">
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible mx-1 h-[14px] w-[1px] shrink-0 bg-divider-regular opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:visible group-hover:opacity-100',
|
||||
isOperationsOpen && 'pointer-events-auto visible opacity-100',
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none invisible shrink-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:visible group-hover:opacity-100',
|
||||
isOperationsOpen && 'pointer-events-auto visible opacity-100',
|
||||
)}
|
||||
>
|
||||
<AppCardOperations
|
||||
app={app}
|
||||
open={isOperationsOpen}
|
||||
webappAuthEnabled={systemFeatures.webapp_auth.enabled}
|
||||
isCurrentWorkspaceEditor={isCurrentWorkspaceEditor}
|
||||
exporting={exporting}
|
||||
@ -561,6 +555,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
|
||||
? 'w-[256px]'
|
||||
: 'w-[216px]'
|
||||
}
|
||||
onOpenChange={setIsOperationsOpen}
|
||||
onEdit={handleOpenEditModal}
|
||||
onDuplicate={handleOpenDuplicateModal}
|
||||
onExport={handleExport}
|
||||
|
||||
Reference in New Issue
Block a user