dify/web/app/components/workflow/operator/zoom-in-out.tsx
yyh c7641bb1ce
refactor(web): unify app-shell bootstrap on TanStack Query + Next.js route conventions (#35394)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-04-20 02:52:08 +00:00

305 lines
9.6 KiB
TypeScript

import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useSuspenseQuery } from '@tanstack/react-query'
import {
Fragment,
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
useReactFlow,
useViewport,
} from 'reactflow'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import {
useNodesSyncDraft,
useWorkflowReadOnly,
} from '../hooks'
import ShortcutsName from '../shortcuts-name'
import TipPopup from './tip-popup'
enum ZoomType {
zoomToFit = 'zoomToFit',
zoomTo25 = 'zoomTo25',
zoomTo50 = 'zoomTo50',
zoomTo75 = 'zoomTo75',
zoomTo100 = 'zoomTo100',
zoomTo200 = 'zoomTo200',
toggleUserComments = 'toggleUserComments',
toggleUserCursors = 'toggleUserCursors',
toggleMiniMap = 'toggleMiniMap',
}
type ZoomInOutProps = {
showMiniMap?: boolean
onToggleMiniMap?: () => void
showUserCursors?: boolean
onToggleUserCursors?: () => void
showUserComments?: boolean
onToggleUserComments?: () => void
isCommentMode?: boolean
}
const ZoomInOut: FC<ZoomInOutProps> = ({
showMiniMap = true,
onToggleMiniMap,
showUserCursors = true,
onToggleUserCursors,
showUserComments = true,
onToggleUserComments,
isCommentMode = false,
}) => {
const { t } = useTranslation()
const {
zoomIn,
zoomOut,
zoomTo,
fitView,
} = useReactFlow()
const { zoom } = useViewport()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const [open, setOpen] = useState(false)
const {
workflowReadOnly,
getWorkflowReadOnly,
} = useWorkflowReadOnly()
const { data: isCollaborationEnabled } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
select: s => s.enable_collaboration_mode,
})
const zoomOptions = [
[
{
key: ZoomType.zoomTo200,
text: '200%',
},
{
key: ZoomType.zoomTo100,
text: '100%',
},
{
key: ZoomType.zoomTo75,
text: '75%',
},
{
key: ZoomType.zoomTo50,
text: '50%',
},
{
key: ZoomType.zoomTo25,
text: '25%',
},
{
key: ZoomType.zoomToFit,
text: t('operator.zoomToFit', { ns: 'workflow' }),
},
],
isCollaborationEnabled
? [
{
key: ZoomType.toggleUserComments,
text: t('operator.showUserComments', { ns: 'workflow' }),
},
{
key: ZoomType.toggleUserCursors,
text: t('operator.showUserCursors', { ns: 'workflow' }),
},
{
key: ZoomType.toggleMiniMap,
text: t('operator.showMiniMap', { ns: 'workflow' }),
},
]
: [
{
key: ZoomType.toggleMiniMap,
text: t('operator.showMiniMap', { ns: 'workflow' }),
},
],
]
const handleZoom = (type: string) => {
if (workflowReadOnly)
return
setOpen(false)
if (type === ZoomType.zoomToFit)
fitView()
if (type === ZoomType.zoomTo25)
zoomTo(0.25)
if (type === ZoomType.zoomTo50)
zoomTo(0.5)
if (type === ZoomType.zoomTo75)
zoomTo(0.75)
if (type === ZoomType.zoomTo100)
zoomTo(1)
if (type === ZoomType.zoomTo200)
zoomTo(2)
if (type === ZoomType.toggleUserComments) {
if (!isCommentMode)
onToggleUserComments?.()
return
}
if (type === ZoomType.toggleUserCursors) {
onToggleUserCursors?.()
return
}
if (type === ZoomType.toggleMiniMap) {
onToggleMiniMap?.()
return
}
handleSyncWorkflowDraft()
}
return (
<div className={`
h-9 cursor-pointer rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg
p-0.5 text-[13px] shadow-lg backdrop-blur-[5px]
hover:bg-state-base-hover
${workflowReadOnly && 'cursor-not-allowed! opacity-50'}
`}
>
<div className="flex h-8 w-[98px] items-center justify-between rounded-lg">
<TipPopup
title={t('operator.zoomOut', { ns: 'workflow' })}
shortcuts={['ctrl', '-']}
>
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom <= 0.25 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
onClick={(e) => {
if (zoom <= 0.25)
return
e.stopPropagation()
zoomOut()
}}
>
<span aria-hidden className="i-ri-zoom-out-line h-4 w-4 text-text-tertiary hover:text-text-secondary" />
</div>
</TipPopup>
<DropdownMenu
open={open}
onOpenChange={setOpen}
>
<DropdownMenuTrigger
disabled={getWorkflowReadOnly()}
className={cn(
'flex h-8 w-[34px] items-center justify-center rounded-lg system-sm-medium text-text-tertiary hover:bg-black/5 hover:text-text-secondary',
open && 'bg-black/5 text-text-secondary',
)}
>
{Number.parseFloat(`${zoom * 100}`).toFixed(0)}
%
</DropdownMenuTrigger>
<DropdownMenuContent
placement="top-start"
sideOffset={4}
alignOffset={-2}
popupClassName="border-0 bg-transparent p-0 shadow-none backdrop-blur-none"
>
<div className="w-[192px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]">
{zoomOptions.map((options, groupIndex) => (
<Fragment key={options[0]!.key}>
{groupIndex !== 0 && (
<DropdownMenuSeparator className="my-0" />
)}
<div className="p-1">
{options.map(option => (
<DropdownMenuItem
key={option.key}
className="justify-between px-3 py-1.5 system-md-regular text-text-secondary"
disabled={option.key === ZoomType.toggleUserComments && isCommentMode}
onClick={() => handleZoom(option.key)}
>
<div className="flex items-center gap-2">
{option.key === ZoomType.toggleUserComments && showUserComments && (
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
)}
{option.key === ZoomType.toggleUserComments && !showUserComments && (
<span aria-hidden className="h-4 w-4" />
)}
{option.key === ZoomType.toggleUserCursors && showUserCursors && (
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
)}
{option.key === ZoomType.toggleUserCursors && !showUserCursors && (
<span aria-hidden className="h-4 w-4" />
)}
{option.key === ZoomType.toggleMiniMap && showMiniMap && (
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
)}
{option.key === ZoomType.toggleMiniMap && !showMiniMap && (
<span aria-hidden className="h-4 w-4" />
)}
{option.key === ZoomType.zoomToFit && (
<span aria-hidden className="i-ri-fullscreen-line h-4 w-4 text-text-tertiary" />
)}
{option.key !== ZoomType.toggleUserComments
&& option.key !== ZoomType.toggleUserCursors
&& option.key !== ZoomType.toggleMiniMap
&& option.key !== ZoomType.zoomToFit && (
<span aria-hidden className="h-4 w-4" />
)}
<span>{option.text}</span>
</div>
<div className="flex items-center space-x-0.5">
{option.key === ZoomType.zoomToFit && (
<ShortcutsName keys={['ctrl', '1']} />
)}
{option.key === ZoomType.zoomTo50 && (
<ShortcutsName keys={['shift', '5']} />
)}
{option.key === ZoomType.zoomTo100 && (
<ShortcutsName keys={['shift', '1']} />
)}
</div>
</DropdownMenuItem>
))}
</div>
</Fragment>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
<TipPopup
title={t('operator.zoomIn', { ns: 'workflow' })}
shortcuts={['ctrl', '+']}
>
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom >= 2 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
onClick={(e) => {
if (zoom >= 2)
return
e.stopPropagation()
zoomIn()
}}
>
<span aria-hidden className="i-ri-zoom-in-line h-4 w-4 text-text-tertiary hover:text-text-secondary" />
</div>
</TipPopup>
</div>
</div>
)
}
export default memo(ZoomInOut)