mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
refactor(webapp): migrate partial overlays (#35825)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
b2dacf0718
commit
f8873ec07b
@ -570,30 +570,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/customize/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/embedded/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/settings/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 3
|
||||
},
|
||||
"regexp/no-unused-capturing-group": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/overview/trigger-card.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@ -8,13 +8,6 @@ vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => mockDocLink,
|
||||
}))
|
||||
|
||||
// Mock window.open
|
||||
const mockWindowOpen = vi.fn()
|
||||
Object.defineProperty(window, 'open', {
|
||||
value: mockWindowOpen,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
describe('CustomizeModal', () => {
|
||||
const defaultProps = {
|
||||
isShow: true,
|
||||
@ -287,7 +280,7 @@ describe('CustomizeModal', () => {
|
||||
|
||||
// User interactions tests - verify user actions trigger expected behaviors
|
||||
describe('User Interactions', () => {
|
||||
it('should call window.open with doc link when way 2 button is clicked', async () => {
|
||||
it('should render the API docs link for way 2', async () => {
|
||||
// Arrange
|
||||
const props = { ...defaultProps }
|
||||
|
||||
@ -298,16 +291,10 @@ describe('CustomizeModal', () => {
|
||||
expect(screen.getByText('appOverview.overview.appInfo.customize.way2.operation')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const way2Button = screen.getByText('appOverview.overview.appInfo.customize.way2.operation').closest('button')
|
||||
expect(way2Button).toBeInTheDocument()
|
||||
fireEvent.click(way2Button!)
|
||||
|
||||
// Assert
|
||||
expect(mockWindowOpen).toHaveBeenCalledTimes(1)
|
||||
expect(mockWindowOpen).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/use-dify/publish/developing-with-apis'),
|
||||
'_blank',
|
||||
)
|
||||
const way2Link = screen.getByRole('link', { name: /way2\.operation/i })
|
||||
expect(way2Link).toHaveAttribute('href', expect.stringContaining('/use-dify/publish/developing-with-apis'))
|
||||
expect(way2Link).toHaveAttribute('target', '_blank')
|
||||
expect(way2Link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should call onClose when modal close button is clicked', async () => {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -25,7 +24,7 @@ const StepNum: FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
|
||||
const GithubIcon = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M5.80078 13.7109C5.80078 13.6406 5.73047 13.5703 5.625 13.5703C5.51953 13.5703 5.44922 13.6406 5.44922 13.7109C5.44922 13.7812 5.51953 13.8516 5.625 13.8164C5.73047 13.8164 5.80078 13.7812 5.80078 13.7109ZM4.71094 13.5352C4.71094 13.6055 4.78125 13.7109 4.88672 13.7109C4.95703 13.7461 5.0625 13.7109 5.09766 13.6406C5.09766 13.5703 5.0625 13.5 4.95703 13.4648C4.85156 13.4297 4.74609 13.4648 4.71094 13.5352ZM6.29297 13.5C6.1875 13.5 6.11719 13.5703 6.11719 13.6758C6.11719 13.7461 6.22266 13.7812 6.32812 13.7461C6.43359 13.7109 6.50391 13.6758 6.46875 13.6055C6.46875 13.5352 6.36328 13.4648 6.29297 13.5ZM8.57812 0C3.72656 0 0 3.72656 0 8.57812C0 12.4805 2.42578 15.8203 5.94141 17.0156C6.39844 17.0859 6.53906 16.8047 6.53906 16.5938C6.53906 16.3477 6.53906 15.1523 6.53906 14.4141C6.53906 14.4141 4.07812 14.9414 3.55078 13.3594C3.55078 13.3594 3.16406 12.3398 2.60156 12.0938C2.60156 12.0938 1.79297 11.5312 2.63672 11.5312C2.63672 11.5312 3.51562 11.6016 4.00781 12.4453C4.78125 13.8164 6.04688 13.4297 6.57422 13.1836C6.64453 12.6211 6.85547 12.2344 7.13672 11.9883C5.16797 11.7773 3.16406 11.4961 3.16406 8.12109C3.16406 7.13672 3.44531 6.67969 4.00781 6.04688C3.90234 5.80078 3.62109 4.88672 4.11328 3.65625C4.81641 3.44531 6.53906 4.60547 6.53906 4.60547C7.24219 4.39453 7.98047 4.32422 8.71875 4.32422C9.49219 4.32422 10.2305 4.39453 10.9336 4.60547C10.9336 4.60547 12.6211 3.41016 13.3594 3.65625C13.8516 4.88672 13.5352 5.80078 13.4648 6.04688C14.0273 6.67969 14.3789 7.13672 14.3789 8.12109C14.3789 11.4961 12.3047 11.7773 10.3359 11.9883C10.6523 12.2695 10.9336 12.7969 10.9336 13.6406C10.9336 14.8008 10.8984 16.2773 10.8984 16.5586C10.8984 16.8047 11.0742 17.0859 11.5312 16.9805C15.0469 15.8203 17.4375 12.4805 17.4375 8.57812C17.4375 3.72656 13.4648 0 8.57812 0ZM3.41016 12.1289C3.33984 12.1641 3.375 12.2695 3.41016 12.3398C3.48047 12.375 3.55078 12.4102 3.62109 12.375C3.65625 12.3398 3.65625 12.2344 3.58594 12.1641C3.51562 12.1289 3.44531 12.0938 3.41016 12.1289ZM3.02344 11.8477C2.98828 11.918 3.02344 11.9531 3.09375 11.9883C3.16406 12.0234 3.23438 12.0234 3.26953 11.9531C3.26953 11.918 3.23438 11.8828 3.16406 11.8477C3.09375 11.8125 3.05859 11.8125 3.02344 11.8477ZM4.14844 13.1133C4.11328 13.1484 4.11328 13.2539 4.21875 13.3242C4.28906 13.3945 4.39453 13.4297 4.42969 13.3594C4.46484 13.3242 4.46484 13.2188 4.39453 13.1484C4.32422 13.0781 4.21875 13.043 4.14844 13.1133ZM3.76172 12.5859C3.69141 12.6211 3.69141 12.7266 3.76172 12.7969C3.83203 12.8672 3.90234 12.9023 3.97266 12.8672C4.00781 12.832 4.00781 12.7266 3.97266 12.6562C3.90234 12.5859 3.83203 12.5508 3.76172 12.5859Z" fill="#1F2A37" />
|
||||
</svg>
|
||||
)
|
||||
@ -43,90 +42,85 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const isChatApp = mode === AppModeEnum.CHAT || mode === AppModeEnum.ADVANCED_CHAT
|
||||
const apiDocLink = docLink('/use-dify/publish/developing-with-apis')
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${prefixCustomize}.title`, { ns: 'appOverview' })}
|
||||
description={t(`${prefixCustomize}.explanation`, { ns: 'appOverview' })}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[640px] max-w-2xl!"
|
||||
closable={true}
|
||||
>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
1
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way1.name`, { ns: 'appOverview' })}</p>
|
||||
<div className="flex py-4">
|
||||
<StepNum>1</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}</div>
|
||||
<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer">
|
||||
<Button>
|
||||
<Dialog open={isShow} onOpenChange={open => !open && onClose()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[640px] overflow-visible">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${prefixCustomize}.title`, { ns: 'appOverview' })}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="mt-2 body-md-regular text-text-secondary">
|
||||
{t(`${prefixCustomize}.explanation`, { ns: 'appOverview' })}
|
||||
</DialogDescription>
|
||||
<DialogCloseButton data-testid="modal-close-button" />
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
1
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way1.name`, { ns: 'appOverview' })}</p>
|
||||
<div className="flex py-4">
|
||||
<StepNum>1</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step1`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step1Tip`, { ns: 'appOverview' })}</div>
|
||||
<Button render={<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer" />}>
|
||||
<GithubIcon className="mr-2 text-text-secondary" />
|
||||
{t(`${prefixCustomize}.way1.step1Operation`, { ns: 'appOverview' })}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-4">
|
||||
<StepNum>2</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}</div>
|
||||
<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer">
|
||||
<Button>
|
||||
<div className="flex pt-4">
|
||||
<StepNum>2</StepNum>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step2`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step2Tip`, { ns: 'appOverview' })}</div>
|
||||
<Button render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" />}>
|
||||
<div className="mr-1.5 border-t-0 border-r-[7px] border-b-12 border-l-[7px] border-solid border-text-primary border-t-transparent border-r-transparent border-l-transparent"></div>
|
||||
<span>{t(`${prefixCustomize}.way1.step2Operation`, { ns: 'appOverview' })}</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-4">
|
||||
<StepNum>3</StepNum>
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}</div>
|
||||
<pre className="box-border overflow-x-scroll rounded-lg border-[0.5px] border-components-panel-border bg-background-section px-4 py-3 text-xs font-medium text-text-secondary select-text">
|
||||
NEXT_PUBLIC_APP_ID=
|
||||
{`'${appId}'`}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_APP_KEY=
|
||||
{'\'<Web API Key From Dify>\''}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_API_URL=
|
||||
{`'${api_base_url}'`}
|
||||
</pre>
|
||||
<div className="flex py-4">
|
||||
<StepNum>3</StepNum>
|
||||
<div className="flex w-full flex-col overflow-hidden">
|
||||
<div className="text-text-primary">{t(`${prefixCustomize}.way1.step3`, { ns: 'appOverview' })}</div>
|
||||
<div className="mt-1 mb-2 text-xs text-text-tertiary">{t(`${prefixCustomize}.way1.step3Tip`, { ns: 'appOverview' })}</div>
|
||||
<pre className="box-border overflow-x-scroll rounded-lg border-[0.5px] border-components-panel-border bg-background-section px-4 py-3 text-xs font-medium text-text-secondary select-text">
|
||||
NEXT_PUBLIC_APP_ID=
|
||||
{`'${appId}'`}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_APP_KEY=
|
||||
{'\'<Web API Key From Dify>\''}
|
||||
{' '}
|
||||
<br />
|
||||
NEXT_PUBLIC_API_URL=
|
||||
{`'${api_base_url}'`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
2
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
docLink('/use-dify/publish/developing-with-apis'),
|
||||
'_blank',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm text-text-secondary">{t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })}</span>
|
||||
<ArrowTopRightOnSquareIcon className="ml-1 h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
2
|
||||
</Tag>
|
||||
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
|
||||
<Button
|
||||
render={<a href={apiDocLink} target="_blank" rel="noopener noreferrer" />}
|
||||
className="mt-2"
|
||||
>
|
||||
<span className="text-sm text-text-secondary">{t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })}</span>
|
||||
<span aria-hidden="true" className="ml-1 i-heroicons-arrow-top-right-on-square h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,17 +1,13 @@
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import {
|
||||
RiClipboardFill,
|
||||
RiClipboardLine,
|
||||
} from '@remixicon/react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { basePath } from '@/utils/var'
|
||||
@ -86,16 +82,18 @@ const prefixEmbedded = 'overview.appInfo.embedded'
|
||||
|
||||
type Option = keyof typeof OPTION_MAP
|
||||
|
||||
type OptionStatus = {
|
||||
iframe: boolean
|
||||
scripts: boolean
|
||||
chromePlugin: boolean
|
||||
const OPTIONS: Option[] = ['iframe', 'scripts', 'chromePlugin']
|
||||
|
||||
const optionIconClassName: Record<Option, string> = {
|
||||
iframe: style.iframeIcon!,
|
||||
scripts: style.scriptsIcon!,
|
||||
chromePlugin: style.chromePluginIcon!,
|
||||
}
|
||||
|
||||
const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, className }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [option, setOption] = useState<Option>('iframe')
|
||||
const [isCopied, setIsCopied] = useState<OptionStatus>({ iframe: false, scripts: false, chromePlugin: false })
|
||||
const [copiedOption, setCopiedOption] = useState<Option | null>(null)
|
||||
|
||||
const { langGeniusVersionInfo } = useAppContext()
|
||||
const themeBuilder = useThemeContext()
|
||||
@ -110,97 +108,98 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam
|
||||
else {
|
||||
copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv))
|
||||
}
|
||||
setIsCopied({ ...isCopied, [option]: true })
|
||||
}
|
||||
|
||||
// when toggle option, reset then copy status
|
||||
const resetCopyStatus = () => {
|
||||
const cache = { ...isCopied }
|
||||
Object.keys(cache).forEach((key) => {
|
||||
cache[key as keyof OptionStatus] = false
|
||||
})
|
||||
setIsCopied(cache)
|
||||
setCopiedOption(option)
|
||||
}
|
||||
|
||||
const navigateToChromeUrl = () => {
|
||||
window.open('https://chrome.google.com/webstore/detail/dify-chatbot/ceehdapohffmjmkdcifjofadiaoeggaf', '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
resetCopyStatus()
|
||||
}, [isShow])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${prefixEmbedded}.title`, { ns: 'appOverview' })}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="w-[640px] max-w-2xl!"
|
||||
wrapperClassName={className}
|
||||
closable={true}
|
||||
<Dialog
|
||||
open={isShow}
|
||||
onOpenChange={(open) => {
|
||||
if (open)
|
||||
return
|
||||
setCopiedOption(null)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<div className="mt-8 mb-4 system-sm-medium text-text-primary">
|
||||
{t(`${prefixEmbedded}.explanation`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-2">
|
||||
{Object.keys(OPTION_MAP).map((v, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
style.option,
|
||||
style[`${v}Icon`],
|
||||
option === v && style.active,
|
||||
)}
|
||||
onClick={() => {
|
||||
setOption(v as Option)
|
||||
resetCopyStatus()
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{option === 'chromePlugin' && (
|
||||
<div className="mt-6 w-full">
|
||||
<div className={cn('inline-flex w-full items-center justify-center gap-2 rounded-lg py-3', 'shrink-0 cursor-pointer bg-primary-600 text-white hover:bg-primary-600/75 hover:shadow-sm')}>
|
||||
<div className={`relative h-4 w-4 ${style.pluginInstallIcon}`}></div>
|
||||
<div className="font-['Inter'] text-sm leading-tight font-medium text-white" onClick={navigateToChromeUrl}>{t(`${prefixEmbedded}.chromePlugin`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<DialogContent className={cn('max-h-[calc(100dvh-2rem)] w-[640px] overflow-visible', className)}>
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(`${prefixEmbedded}.title`, { ns: 'appOverview' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton />
|
||||
<div className="mt-8 mb-4 system-sm-medium text-text-primary">
|
||||
{t(`${prefixEmbedded}.explanation`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('inline-flex w-full flex-col items-start justify-start rounded-lg border-[0.5px] border-components-panel-border bg-background-section', 'mt-6')}>
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch rounded-t-lg bg-background-section-burn py-1 pr-1 pl-3">
|
||||
<div className="shrink-0 grow system-sm-medium text-text-secondary">
|
||||
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
|
||||
<div className="flex flex-wrap items-center justify-between gap-y-2">
|
||||
{OPTIONS.map((v) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={v}
|
||||
aria-label={t(`${prefixEmbedded}.${v}`, { ns: 'appOverview' }) || v}
|
||||
className={cn(
|
||||
style.option,
|
||||
optionIconClassName[v],
|
||||
option === v && style.active,
|
||||
)}
|
||||
onClick={() => {
|
||||
setOption(v)
|
||||
setCopiedOption(null)
|
||||
}}
|
||||
>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{option === 'chromePlugin' && (
|
||||
<div className="mt-6 w-full">
|
||||
<button
|
||||
type="button"
|
||||
className={cn('inline-flex w-full items-center justify-center gap-2 rounded-lg py-3', 'shrink-0 bg-primary-600 text-white hover:bg-primary-600/75 hover:shadow-sm')}
|
||||
onClick={navigateToChromeUrl}
|
||||
>
|
||||
<div className={`relative h-4 w-4 ${style.pluginInstallIcon}`}></div>
|
||||
<div className="font-['Inter'] text-sm leading-tight font-medium text-white">{t(`${prefixEmbedded}.chromePlugin`, { ns: 'appOverview' })}</div>
|
||||
</button>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton>
|
||||
<div
|
||||
)}
|
||||
<div className={cn('inline-flex w-full flex-col items-start justify-start rounded-lg border-[0.5px] border-components-panel-border bg-background-section', 'mt-6')}>
|
||||
<div className="inline-flex items-center justify-start gap-2 self-stretch rounded-t-lg bg-background-section-burn py-1 pr-1 pl-3">
|
||||
<div className="shrink-0 grow system-sm-medium text-text-secondary">
|
||||
{t(`${prefixEmbedded}.${option}`, { ns: 'appOverview' })}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={(copiedOption === option
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
onClick={onClickCopy}
|
||||
>
|
||||
{isCopied[option] && <RiClipboardFill className="h-4 w-4" />}
|
||||
{!isCopied[option] && <RiClipboardLine className="h-4 w-4" />}
|
||||
</div>
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{(isCopied[option]
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex w-full items-start justify-start gap-2 overflow-x-auto p-3">
|
||||
<div className="shrink grow basis-0 font-mono text-[13px] leading-tight text-text-secondary">
|
||||
<pre className="select-text">{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
|
||||
{copiedOption === option && <span aria-hidden="true" className="i-ri-clipboard-fill h-4 w-4" />}
|
||||
{copiedOption !== option && <span aria-hidden="true" className="i-ri-clipboard-line h-4 w-4" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
{(copiedOption === option
|
||||
? t(`${prefixEmbedded}.copied`, { ns: 'appOverview' })
|
||||
: t(`${prefixEmbedded}.copy`, { ns: 'appOverview' })) || ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex max-h-[clamp(180px,calc(100dvh-320px),360px)] w-full items-start justify-start gap-2 overflow-auto p-3">
|
||||
<div className="shrink grow basis-0 font-mono text-[13px] leading-tight text-text-secondary">
|
||||
<pre className="select-text">{OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { baseProviderContextValue } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
@ -224,54 +224,51 @@ describe('SettingsModal', () => {
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear the delayed hide-more timer when the modal unmounts after closing', () => {
|
||||
vi.useFakeTimers()
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
|
||||
const { unmount } = renderSettingsModal()
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
unmount()
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled()
|
||||
vi.runAllTimers()
|
||||
})
|
||||
|
||||
it('should replace the pending hide-more timer and clear the ref after the timeout completes', async () => {
|
||||
const hideCallbacks: Array<() => void> = []
|
||||
const originalSetTimeout = globalThis.setTimeout
|
||||
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout').mockImplementation(((
|
||||
callback: TimerHandler,
|
||||
delay?: number,
|
||||
...args: unknown[]
|
||||
) => {
|
||||
if (delay === 200) {
|
||||
hideCallbacks.push(() => {
|
||||
if (typeof callback === 'function')
|
||||
callback(...args)
|
||||
})
|
||||
return hideCallbacks.length as unknown as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
return originalSetTimeout(callback, delay, ...args)
|
||||
}) as unknown as typeof setTimeout)
|
||||
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
|
||||
it('should collapse the expanded settings section immediately when closing', () => {
|
||||
renderSettingsModal()
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
})
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled()
|
||||
expect(hideCallbacks.length).toBeGreaterThanOrEqual(2)
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
act(() => {
|
||||
hideCallbacks.at(-1)?.()
|
||||
})
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
setTimeoutSpy.mockRestore()
|
||||
clearTimeoutSpy.mockRestore()
|
||||
it('should reset local form state when the controlled dialog reopens', () => {
|
||||
const { rerender } = render(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry'))
|
||||
expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={false}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
rerender(
|
||||
<SettingsModal
|
||||
isChat
|
||||
isShow={true}
|
||||
appInfo={mockAppInfo}
|
||||
onClose={mockOnClose}
|
||||
onSave={mockOnSave}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open the pricing modal from the copyright upgrade badge for sandbox plans', async () => {
|
||||
|
||||
@ -5,23 +5,20 @@ import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppIconType, AppSSO, Language } from '@/types/app'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -62,31 +59,20 @@ type SelectOption = {
|
||||
name: string
|
||||
}
|
||||
|
||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
isChat,
|
||||
appInfo,
|
||||
isShow = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const createInputInfo = (appInfo: ISettingsModalProps['appInfo']) => {
|
||||
const {
|
||||
title,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
icon_url,
|
||||
description,
|
||||
chat_color_theme,
|
||||
chat_color_theme_inverted,
|
||||
copyright,
|
||||
privacy_policy,
|
||||
custom_disclaimer,
|
||||
default_language,
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
} = appInfo.site
|
||||
const [inputInfo, setInputInfo] = useState({
|
||||
|
||||
return {
|
||||
title,
|
||||
desc: description,
|
||||
chatColorTheme: chat_color_theme,
|
||||
@ -98,18 +84,57 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
enable_sso: appInfo.enable_sso,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const createAppIcon = (appInfo: ISettingsModalProps['appInfo']): AppIconSelection => {
|
||||
const { icon_type, icon, icon_background, icon_url } = appInfo.site
|
||||
|
||||
return icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! }
|
||||
}
|
||||
|
||||
const getSettingsResetKey = (appInfo: ISettingsModalProps['appInfo']) => JSON.stringify([
|
||||
appInfo.id,
|
||||
appInfo.enable_sso,
|
||||
appInfo.site.title,
|
||||
appInfo.site.description,
|
||||
appInfo.site.chat_color_theme,
|
||||
appInfo.site.chat_color_theme_inverted,
|
||||
appInfo.site.copyright,
|
||||
appInfo.site.privacy_policy,
|
||||
appInfo.site.custom_disclaimer,
|
||||
appInfo.site.default_language,
|
||||
appInfo.site.icon_type,
|
||||
appInfo.site.icon,
|
||||
appInfo.site.icon_background,
|
||||
appInfo.site.icon_url,
|
||||
appInfo.site.show_workflow_steps,
|
||||
appInfo.site.use_icon_as_answer_icon,
|
||||
])
|
||||
|
||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
isChat,
|
||||
appInfo,
|
||||
isShow = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const { default_language } = appInfo.site
|
||||
const nextInputInfo = createInputInfo(appInfo)
|
||||
const nextAppIcon = createAppIcon(appInfo)
|
||||
const settingsResetKey = getSettingsResetKey(appInfo)
|
||||
const [inputInfo, setInputInfo] = useState(nextInputInfo)
|
||||
const [language, setLanguage] = useState(default_language)
|
||||
const [saveLoading, setSaveLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
const hideMoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(
|
||||
icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! },
|
||||
)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>(nextAppIcon)
|
||||
const [previousIsShow, setPreviousIsShow] = useState(isShow)
|
||||
const [previousSettingsResetKey, setPreviousSettingsResetKey] = useState(settingsResetKey)
|
||||
|
||||
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
@ -123,43 +148,21 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
|
||||
|
||||
useEffect(() => {
|
||||
setInputInfo({
|
||||
title,
|
||||
desc: description,
|
||||
chatColorTheme: chat_color_theme,
|
||||
chatColorThemeInverted: chat_color_theme_inverted,
|
||||
copyright,
|
||||
copyrightSwitchValue: !!copyright,
|
||||
privacyPolicy: privacy_policy,
|
||||
customDisclaimer: custom_disclaimer,
|
||||
show_workflow_steps,
|
||||
use_icon_as_answer_icon,
|
||||
enable_sso: appInfo.enable_sso,
|
||||
})
|
||||
setLanguage(default_language)
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
}, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hideMoreTimerRef.current) {
|
||||
clearTimeout(hideMoreTimerRef.current)
|
||||
hideMoreTimerRef.current = null
|
||||
}
|
||||
const shouldResetForm = isShow && (!previousIsShow || settingsResetKey !== previousSettingsResetKey)
|
||||
if (isShow !== previousIsShow || shouldResetForm) {
|
||||
setPreviousIsShow(isShow)
|
||||
if (shouldResetForm) {
|
||||
setInputInfo(nextInputInfo)
|
||||
setLanguage(default_language)
|
||||
setAppIcon(nextAppIcon)
|
||||
setIsShowMore(false)
|
||||
setPreviousSettingsResetKey(settingsResetKey)
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
const onHide = () => {
|
||||
onClose()
|
||||
if (hideMoreTimerRef.current)
|
||||
clearTimeout(hideMoreTimerRef.current)
|
||||
hideMoreTimerRef.current = setTimeout(() => {
|
||||
setIsShowMore(false)
|
||||
hideMoreTimerRef.current = null
|
||||
}, 200)
|
||||
setIsShowMore(false)
|
||||
}
|
||||
|
||||
const onClickSave = async () => {
|
||||
@ -172,7 +175,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
if (hex === null || hex?.length === 0)
|
||||
return true
|
||||
|
||||
const regex = /#([A-F0-9]{6})/i
|
||||
const regex = /#[A-F0-9]{6}/i
|
||||
const check = regex.test(hex)
|
||||
return check
|
||||
}
|
||||
@ -240,245 +243,250 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
closable={false}
|
||||
onClose={onHide}
|
||||
className="max-w-[520px] p-0"
|
||||
>
|
||||
{/* header */}
|
||||
<div className="pt-5 pr-5 pb-3 pl-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="grow title-2xl-semi-bold text-text-primary">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</div>
|
||||
<ActionButton className="shrink-0" onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
<Dialog open={isShow} onOpenChange={open => !open && onHide()}>
|
||||
<DialogContent className="max-h-[calc(100dvh-2rem)] w-[520px] overflow-visible p-0">
|
||||
{/* header */}
|
||||
<div className="pt-5 pr-5 pb-3 pl-6">
|
||||
<div className="flex items-center gap-1">
|
||||
<DialogTitle className="grow title-2xl-semi-bold text-text-primary">{t(`${prefixSettings}.title`, { ns: 'appOverview' })}</DialogTitle>
|
||||
<DialogCloseButton className="relative top-auto right-auto shrink-0" />
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-0.5 system-xs-regular text-text-tertiary">
|
||||
<span>{t(`${prefixSettings}.modalTip`, { ns: 'appOverview' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* form body */}
|
||||
<div className="space-y-5 px-6 py-3">
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn('mb-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')}
|
||||
placeholder={t('appNamePlaceholder', { ns: 'app' }) || ''}
|
||||
{/* form body */}
|
||||
<div className="space-y-5 px-6 py-3">
|
||||
{/* name & icon */}
|
||||
<div className="flex gap-4">
|
||||
<div className="grow">
|
||||
<div className={cn('mb-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}</div>
|
||||
<Input
|
||||
className="w-full"
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')}
|
||||
placeholder={t('appNamePlaceholder', { ns: 'app' }) || ''}
|
||||
/>
|
||||
</div>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
className="mt-2 cursor-pointer"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<AppIcon
|
||||
size="xxl"
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
className="mt-2 cursor-pointer"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
{/* description */}
|
||||
<div className="relative">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
{/* description */}
|
||||
<div className="relative">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}</div>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.desc}
|
||||
onChange={e => onDesChange(e.target.value)}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
<Divider className="my-0 h-px" />
|
||||
{/* answer icon */}
|
||||
{isChat && (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
</div>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={(nextValue) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
setLanguage(nextValue as Language)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-[200px]">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Input
|
||||
className="mb-1 w-[200px]"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onChange={onChange('chatColorTheme')}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<Switch checked={inputInfo.chatColorThemeInverted} onCheckedChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t('answerIcon.title', { ns: 'app' })}</div>
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<Switch
|
||||
checked={inputInfo.use_icon_as_answer_icon}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t('answerIcon.description', { ns: 'app' })}</p>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
)}
|
||||
{/* language */}
|
||||
<div className="flex items-center">
|
||||
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.language`, { ns: 'appOverview' })}</div>
|
||||
<Select
|
||||
value={selectedLanguage?.value ?? null}
|
||||
onValueChange={(nextValue) => {
|
||||
if (!nextValue)
|
||||
return
|
||||
setLanguage(nextValue as Language)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-[200px]">
|
||||
{selectedLanguage?.name ?? t('placeholder.select', { ns: 'common' })}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-(--anchor-width)">
|
||||
{languageOptions.map(item => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
<SelectItemText>{item.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{/* theme color */}
|
||||
{isChat && (
|
||||
<div className="flex items-center">
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.chatColorTheme`, { ns: 'appOverview' })}</div>
|
||||
<div className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.chatColorThemeDesc`, { ns: 'appOverview' })}</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Input
|
||||
className="mb-1 w-[200px]"
|
||||
value={inputInfo.chatColorTheme ?? ''}
|
||||
onChange={onChange('chatColorTheme')}
|
||||
placeholder="E.g #A020F0"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={cn('body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.chatColorThemeInverted`, { ns: 'appOverview' })}</p>
|
||||
<Switch checked={inputInfo.chatColorThemeInverted} onCheckedChange={v => setInputInfo({ ...inputInfo, chatColorThemeInverted: v })}></Switch>
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
&
|
||||
{' '}
|
||||
{t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })}
|
||||
</p>
|
||||
</div>
|
||||
<span aria-hidden="true" className="ml-1 i-ri-arrow-right-s-line h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* workflow detail */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`, { ns: 'appOverview' })}</div>
|
||||
<Switch
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
checked={inputInfo.show_workflow_steps}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
)}
|
||||
{/* more settings */}
|
||||
{isShowMore && (
|
||||
<>
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{enableBilling && isFreePlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadge size="s" color="blue" allowHover={true} onClick={handlePlanClick}>
|
||||
<span aria-hidden="true" className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{webappCopyrightEnabled
|
||||
? (
|
||||
<Switch
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div>
|
||||
<Switch
|
||||
disabled
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent className="w-[180px]">
|
||||
{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className="flex justify-end p-6 pt-5">
|
||||
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
{showAppIconPicker && (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(createAppIcon(appInfo))
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })}</p>
|
||||
</div>
|
||||
{/* more settings switch */}
|
||||
<Divider className="my-0 h-px" />
|
||||
{!isShowMore && (
|
||||
<div className="flex cursor-pointer items-center" onClick={() => setIsShowMore(true)}>
|
||||
<div className="grow">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
{t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })}
|
||||
{' '}
|
||||
&
|
||||
{' '}
|
||||
{t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })}
|
||||
</p>
|
||||
</div>
|
||||
<RiArrowRightSLine className="ml-1 h-4 w-4 shrink-0 text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
{/* more settings */}
|
||||
{isShowMore && (
|
||||
<>
|
||||
{/* copyright */}
|
||||
<div className="w-full">
|
||||
<div className="flex items-center">
|
||||
<div className="flex grow items-center">
|
||||
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}</div>
|
||||
{/* upgrade button */}
|
||||
{enableBilling && isFreePlan && (
|
||||
<div className="h-[18px] select-none">
|
||||
<PremiumBadge size="s" color="blue" allowHover={true} onClick={handlePlanClick}>
|
||||
<SparklesSoft className="flex h-3.5 w-3.5 items-center py-px pl-[3px] text-components-premium-badge-indigo-text-stop-0" />
|
||||
<div className="system-xs-medium">
|
||||
<span className="p-1">
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
disabled={webappCopyrightEnabled}
|
||||
popupContent={
|
||||
<div className="w-[180px]">{t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })}</div>
|
||||
}
|
||||
asChild={false}
|
||||
>
|
||||
<Switch
|
||||
disabled={!webappCopyrightEnabled}
|
||||
checked={inputInfo.copyrightSwitchValue}
|
||||
onCheckedChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="pb-0.5 body-xs-regular text-text-tertiary">{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}</p>
|
||||
{inputInfo.copyrightSwitchValue && (
|
||||
<Input
|
||||
className="mt-2 h-10"
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* privacy policy */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
ns="appOverview"
|
||||
components={{ privacyPolicyLink: <Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer" className="text-text-accent" /> }}
|
||||
/>
|
||||
</p>
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
{/* custom disclaimer */}
|
||||
<div className="w-full">
|
||||
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}</div>
|
||||
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}</p>
|
||||
<Textarea
|
||||
className="mt-1"
|
||||
value={inputInfo.customDisclaimer}
|
||||
onChange={onChange('customDisclaimer')}
|
||||
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* footer */}
|
||||
<div className="flex justify-end p-6 pt-5">
|
||||
<Button className="mr-2" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button variant="primary" onClick={onClickSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
{showAppIconPicker && (
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user