feat(webapp): customize

This commit is contained in:
yyh 2026-06-23 20:01:50 +08:00
parent 238b9202d6
commit f11b09abf1
No known key found for this signature in database
3 changed files with 57 additions and 5 deletions

View File

@ -13,7 +13,8 @@ type IShareLinkProps = {
onClose: () => void
api_base_url: string
appId: string
mode: AppModeEnum
mode?: AppModeEnum
sourceCodeRepository?: 'webapp-conversation' | 'webapp-text-generator'
}
const StepNum: FC<{ children: React.ReactNode }> = ({ children }) => (
@ -38,10 +39,12 @@ const CustomizeModal: FC<IShareLinkProps> = ({
appId,
api_base_url,
mode,
sourceCodeRepository,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
const isChatApp = mode === AppModeEnum.CHAT || mode === AppModeEnum.ADVANCED_CHAT
const repository = sourceCodeRepository ?? (isChatApp ? 'webapp-conversation' : 'webapp-text-generator')
const apiDocLink = docLink('/use-dify/publish/developing-with-apis')
return (
@ -67,7 +70,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<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 nativeButton={false} render={<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target="_blank" rel="noopener noreferrer" />}>
<Button nativeButton={false} render={<a href={`https://github.com/langgenius/${repository}`} target="_blank" rel="noopener noreferrer" aria-label={t(`${prefixCustomize}.way1.step1Operation`, { ns: 'appOverview' })} />}>
<GithubIcon className="mr-2 text-text-secondary" />
{t(`${prefixCustomize}.way1.step1Operation`, { ns: 'appOverview' })}
</Button>
@ -78,7 +81,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<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 nativeButton={false} render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" />}>
<Button nativeButton={false} render={<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target="_blank" rel="noopener noreferrer" aria-label={t(`${prefixCustomize}.way1.step2Operation`, { ns: 'appOverview' })} />}>
<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>
@ -114,7 +117,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
<p className="my-2 system-sm-medium text-text-secondary">{t(`${prefixCustomize}.way2.name`, { ns: 'appOverview' })}</p>
<Button
nativeButton={false}
render={<a href={apiDocLink} target="_blank" rel="noopener noreferrer" />}
render={<a href={apiDocLink} target="_blank" rel="noopener noreferrer" aria-label={t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })} />}
className="mt-2"
>
<span className="text-sm text-text-secondary">{t(`${prefixCustomize}.way2.operation`, { ns: 'appOverview' })}</span>

View File

@ -115,6 +115,7 @@ function createAgent(overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppD
mode: 'agent',
name: 'Support Agent',
app_id: 'app-1',
api_base_url: 'https://api.example.test/v1',
access_mode: 'sso_verified',
site: {
access_token: 'site-token',
@ -185,6 +186,29 @@ describe('Agent access surface cards', () => {
})
})
})
it('should open the customize dialog with the backing app id and API base URL', async () => {
const user = userEvent.setup()
renderWithQueryClient(
<WebAppAccessCard agent={createAgent()} agentId="agent-1" isLoading={false} />,
)
await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.customize' }))
const dialog = await screen.findByRole('dialog', { name: 'appOverview.overview.appInfo.customize.title' })
expect(dialog).toHaveTextContent(/NEXT_PUBLIC_APP_ID=\s*'app-1'/)
expect(dialog).toHaveTextContent(/NEXT_PUBLIC_API_URL=\s*'https:\/\/api\.example\.test\/v1'/)
expect(within(dialog).getByRole('button', { name: /appOverview\.overview\.appInfo\.customize\.way1\.step1Operation/ })).toHaveAttribute('href', 'https://github.com/langgenius/webapp-conversation')
})
it('should keep customize disabled until the generated contract provides the required fields', () => {
renderWithQueryClient(
<WebAppAccessCard agent={createAgent({ api_base_url: null })} agentId="agent-1" isLoading={false} />,
)
expect(screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.actions.customize' })).toBeDisabled()
})
})
describe('Service API access', () => {

View File

@ -4,7 +4,9 @@ import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/t
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import CustomizeModal from '@/app/components/app/overview/customize'
import ShareQRCode from '@/app/components/base/qrcode'
import { AccessMode } from '@/models/access-control'
import { consoleQuery } from '@/service/client'
@ -29,10 +31,18 @@ export function WebAppAccessCard({
const { t: tCommon } = useTranslation('common')
const queryClient = useQueryClient()
const appId = agent?.app_id
const apiBaseUrl = agent?.api_base_url
const webAppUrl = getAgentWebAppUrl(agent)
const isEnabled = Boolean(agent?.enable_site)
const canManageWebApp = Boolean(appId)
const customizeConfig = appId && apiBaseUrl
? {
apiBaseUrl,
appId,
}
: null
const showSsoBadge = agent?.access_mode === AccessMode.EXTERNAL_MEMBERS
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
const toggleSiteMutation = useMutation(consoleQuery.apps.byAppId.siteEnable.post.mutationOptions({
onSuccess: (_updatedApp, variables) => {
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
@ -156,7 +166,13 @@ export function WebAppAccessCard({
<span aria-hidden className="i-ri-window-line size-4" />
{t('agentDetail.access.webApp.actions.embedded')}
</Button>
<Button variant="secondary" size="medium" className="gap-1.5 px-3" disabled>
<Button
variant="secondary"
size="medium"
className="gap-1.5 px-3"
disabled={!customizeConfig}
onClick={() => setShowCustomizeModal(true)}
>
<span aria-hidden className="i-ri-paint-brush-line size-4" />
{t('agentDetail.access.webApp.actions.customize')}
</Button>
@ -164,6 +180,15 @@ export function WebAppAccessCard({
<span aria-hidden className="i-ri-equalizer-2-line size-4" />
{t('agentDetail.access.webApp.actions.settings')}
</Button>
{customizeConfig && (
<CustomizeModal
isShow={showCustomizeModal}
onClose={() => setShowCustomizeModal(false)}
appId={customizeConfig.appId}
api_base_url={customizeConfig.apiBaseUrl}
sourceCodeRepository="webapp-conversation"
/>
)}
</AccessSurfaceCard>
)
}