mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
'use client'
|
|
import type { AppIconType } from '@/types/app'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
|
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
|
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
|
import { Input } from '@langgenius/dify-ui/input'
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { useAtomValue } from 'jotai'
|
|
import * as React from 'react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import AppIcon from '@/app/components/base/app-icon'
|
|
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
|
import { deploymentEditionAtom } from '@/features/system-features/state'
|
|
import { consoleQuery } from '@/service/client'
|
|
import AppIconPicker from '../../base/app-icon-picker'
|
|
|
|
export type DuplicateAppModalProps = {
|
|
appName: string
|
|
icon_type: AppIconType | null
|
|
icon: string
|
|
icon_background?: string | null
|
|
icon_url?: string | null
|
|
show: boolean
|
|
onConfirm: (info: {
|
|
name: string
|
|
icon_type: AppIconType
|
|
icon: string
|
|
icon_background?: string | null
|
|
}) => Promise<void>
|
|
onHide: () => void
|
|
}
|
|
|
|
const DuplicateAppModal = ({
|
|
appName,
|
|
icon_type,
|
|
icon,
|
|
icon_background,
|
|
icon_url,
|
|
show = false,
|
|
onConfirm,
|
|
onHide,
|
|
}: DuplicateAppModalProps) => {
|
|
const { t } = useTranslation()
|
|
|
|
const [name, setName] = React.useState(appName)
|
|
|
|
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
|
const [appIcon, setAppIcon] = useState(
|
|
icon_type === 'image'
|
|
? { type: 'image' as const, url: icon_url, fileId: icon }
|
|
: { type: 'emoji' as const, icon, background: icon_background },
|
|
)
|
|
|
|
const deploymentEdition = useAtomValue(deploymentEditionAtom)
|
|
const { data: appQuota } = useQuery(
|
|
consoleQuery.features.get.queryOptions({
|
|
enabled: deploymentEdition === 'CLOUD',
|
|
select: (data) => data.apps,
|
|
}),
|
|
)
|
|
const isAppQuotaUnavailable = deploymentEdition === 'CLOUD' && appQuota === undefined
|
|
// A limit of 0 means unlimited.
|
|
const isAppsFull =
|
|
deploymentEdition === 'CLOUD' &&
|
|
appQuota !== undefined &&
|
|
appQuota.limit > 0 &&
|
|
appQuota.size >= appQuota.limit
|
|
|
|
const submit = () => {
|
|
if (isAppQuotaUnavailable || isAppsFull) return
|
|
|
|
if (!name.trim()) {
|
|
toast.error(t(($) => $['appCustomize.nameRequired'], { ns: 'explore' }))
|
|
return
|
|
}
|
|
onConfirm({
|
|
name,
|
|
icon_type: appIcon.type,
|
|
icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
|
|
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
|
})
|
|
onHide()
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Dialog
|
|
open={show}
|
|
onOpenChange={(open) => {
|
|
if (!open) onHide()
|
|
}}
|
|
>
|
|
<DialogContent className="w-full max-w-120! overflow-hidden! border-none px-8 text-left align-middle">
|
|
<IconButton
|
|
size="lg"
|
|
className="absolute top-4 right-4"
|
|
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
|
onClick={onHide}
|
|
>
|
|
<span aria-hidden="true" className="i-ri-close-line size-4" />
|
|
</IconButton>
|
|
<DialogTitle className="relative mt-3 mb-9 text-xl leading-7.5 font-semibold text-text-primary">
|
|
{t(($) => $.duplicateTitle, { ns: 'app' })}
|
|
</DialogTitle>
|
|
<form
|
|
onSubmit={(event) => {
|
|
event.preventDefault()
|
|
submit()
|
|
}}
|
|
>
|
|
<div className="mb-9 system-sm-regular text-text-secondary">
|
|
<Field className="gap-2" name="name">
|
|
<FieldLabel className="py-0 system-md-medium">
|
|
{t(($) => $['appCustomize.subTitle'], { ns: 'explore' })}
|
|
</FieldLabel>
|
|
<div className="flex items-center justify-between space-x-2">
|
|
<button
|
|
type="button"
|
|
aria-label={`${t(($) => $['operation.edit'], { ns: 'common' })} ${t(($) => $['appCustomize.subTitle'], { ns: 'explore' })}`}
|
|
onClick={() => {
|
|
setShowAppIconPicker(true)
|
|
}}
|
|
className="shrink-0 cursor-pointer rounded-[10px] focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
|
>
|
|
<AppIcon
|
|
size="large"
|
|
iconType={appIcon.type}
|
|
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
|
|
background={appIcon.type === 'image' ? undefined : appIcon.background}
|
|
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
|
|
/>
|
|
</button>
|
|
<Input
|
|
autoComplete="off"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className="h-10"
|
|
placeholder={t(($) => $['placeholder.input'], { ns: 'common' }) || ''}
|
|
/>
|
|
</div>
|
|
</Field>
|
|
{isAppsFull && <AppsFull className="mt-4" loc="app-duplicate-create" />}
|
|
</div>
|
|
<div className="flex flex-row-reverse">
|
|
<Button
|
|
type="submit"
|
|
disabled={isAppQuotaUnavailable || isAppsFull}
|
|
className="ml-2 w-24"
|
|
variant="primary"
|
|
>
|
|
{t(($) => $.duplicate, { ns: 'app' })}
|
|
</Button>
|
|
<Button type="button" className="w-24" onClick={onHide}>
|
|
{t(($) => $['operation.cancel'], { ns: 'common' })}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
{showAppIconPicker && (
|
|
<AppIconPicker
|
|
open={showAppIconPicker}
|
|
initialEmoji={
|
|
appIcon.type === 'emoji'
|
|
? { icon: appIcon.icon, background: appIcon.background }
|
|
: undefined
|
|
}
|
|
onOpenChange={setShowAppIconPicker}
|
|
onSelect={(payload) => {
|
|
setAppIcon(payload)
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default DuplicateAppModal
|