diff --git a/e2e/features/step-definitions/agent-v2/access-point-web-app.steps.ts b/e2e/features/step-definitions/agent-v2/access-point-web-app.steps.ts index e5de7e1ac26..0b601b950f2 100644 --- a/e2e/features/step-definitions/agent-v2/access-point-web-app.steps.ts +++ b/e2e/features/step-definitions/agent-v2/access-point-web-app.steps.ts @@ -155,26 +155,26 @@ Then('I should see the Agent v2 Embedded configuration dialog', async function ( }) When('I open Agent v2 Web app customization', async function (this: DifyWorld) { - await getWebAppCard(this).getByRole('button', { name: 'Customize' }).click() + await getWebAppCard(this).getByRole('button', { name: 'Custom Frontend' }).click() }) Then('I should see the Agent v2 Web app customization dialog', async function (this: DifyWorld) { - const dialog = getDialog(this, 'Customize AI web app') + const dialog = getDialog(this, 'Custom Frontend') await expect(dialog).toBeVisible() - await expect(dialog.getByText('Customize AI web app')).toBeVisible() + await expect(dialog.getByText('Custom Frontend')).toBeVisible() await expect(dialog.getByText(/NEXT_PUBLIC_APP_ID|NEXT_PUBLIC_API_URL/)).toBeVisible() }) When('I open Agent v2 Web app settings', async function (this: DifyWorld) { - await getWebAppCard(this).getByRole('button', { name: 'Settings' }).click() + await getWebAppCard(this).getByRole('button', { name: 'Branding' }).click() }) Then('I should see the Agent v2 Web app settings dialog', async function (this: DifyWorld) { - const dialog = getDialog(this, 'Web App Settings') + const dialog = getDialog(this, 'Branding') await expect(dialog).toBeVisible() - await expect(dialog.getByRole('heading', { name: 'Web App Settings' })).toBeVisible() + await expect(dialog.getByRole('heading', { name: 'Branding' })).toBeVisible() await expect(dialog.getByText('web app Name')).toBeVisible() await expect(dialog.getByText('web app Description')).toBeVisible() }) diff --git a/e2e/features/step-definitions/agent-v2/access-point.steps.ts b/e2e/features/step-definitions/agent-v2/access-point.steps.ts index ee9e41e7390..11edc563a43 100644 --- a/e2e/features/step-definitions/agent-v2/access-point.steps.ts +++ b/e2e/features/step-definitions/agent-v2/access-point.steps.ts @@ -75,8 +75,8 @@ Then('I should see the Agent v2 Access Point overview', async function (this: Di await expect(accessRegion.getByLabel('Toggle Web app access')).toBeVisible() await expect(accessRegion.getByRole('link', { name: 'Launch' })).toBeVisible() await expect(accessRegion.getByRole('button', { name: 'Embedded' })).toBeVisible() - await expect(accessRegion.getByRole('button', { name: 'Customize' })).toBeVisible() - await expect(accessRegion.getByRole('button', { name: 'Settings' })).toBeVisible() + await expect(accessRegion.getByRole('button', { name: 'Custom Frontend' })).toBeVisible() + await expect(accessRegion.getByRole('button', { name: 'Branding' })).toBeVisible() await expect(accessRegion.getByRole('heading', { name: 'Backend service API' })).toBeVisible() await expect(accessRegion.getByText('Service API Endpoint')).toBeVisible() await expect(accessRegion.getByLabel('Copy service API endpoint')).toBeVisible() diff --git a/web/app/components/app/overview/__tests__/app-card-sections.spec.tsx b/web/app/components/app/overview/__tests__/app-card-sections.spec.tsx index 425110e4330..8579dd0ff9e 100644 --- a/web/app/components/app/overview/__tests__/app-card-sections.spec.tsx +++ b/web/app/components/app/overview/__tests__/app-card-sections.spec.tsx @@ -117,9 +117,45 @@ describe('app-card-sections', () => { expect(onLaunch).toHaveBeenCalledTimes(1) expect(onLaunchConfig).toHaveBeenCalledTimes(1) + expect(screen.getByText('overview.appInfo.launch')).not.toHaveAttribute('title') expect(screen.getByRole('button', { name: /overview\.appInfo\.embedded\.entry/i })).toBeInTheDocument() }) + it('should expose native titles only for truncated operation labels', () => { + const operations = createAppCardOperations({ + operationKeys: ['launch', 'embedded', 'customize', 'settings', 'develop'], + t: t as never, + runningStatus: true, + triggerModeDisabled: false, + onLaunch: vi.fn(), + onEmbedded: vi.fn(), + onCustomize: vi.fn(), + onSettings: vi.fn(), + onDevelop: vi.fn(), + }) + + render() + + const untruncatedLabels = [ + 'overview.appInfo.launch', + 'overview.appInfo.embedded.entry', + 'overview.apiInfo.doc', + ] + untruncatedLabels.forEach((label) => { + expect(screen.getByText(label)).toBeInTheDocument() + expect(screen.getByText(label)).not.toHaveAttribute('title') + }) + + const truncatedLabels = [ + 'overview.appInfo.customize.entry', + 'overview.appInfo.settings.entry', + ] + truncatedLabels.forEach((label) => { + expect(screen.getByText(label)).toBeInTheDocument() + expect(screen.getByRole('button', { name: label })).toHaveAttribute('title', label) + }) + }) + it('should keep customize available for web app cards that are not completion or workflow apps', () => { const operations = createAppCardOperations({ operationKeys: ['customize'], @@ -140,7 +176,7 @@ describe('app-card-sections', () => { />, ) - expect(screen.getByText('overview.appInfo.customize.entry')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'overview.appInfo.customize.entry' })).toHaveAttribute('title', 'overview.appInfo.customize.entry') expect(AppModeEnum.CHAT).toBe('chat') }) diff --git a/web/app/components/app/overview/app-card-sections.tsx b/web/app/components/app/overview/app-card-sections.tsx index 6fd38183f16..a1bf7ceb002 100644 --- a/web/app/components/app/overview/app-card-sections.tsx +++ b/web/app/components/app/overview/app-card-sections.tsx @@ -19,6 +19,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent, @@ -30,7 +31,7 @@ import { TooltipContent, TooltipTrigger, } from '@langgenius/dify-ui/tooltip' -import { RiArrowRightSLine, RiBookOpenLine, RiBuildingLine, RiEqualizer2Line, RiExternalLinkLine, RiGlobalLine, RiLockLine, RiPaintBrushLine, RiSettings2Line, RiVerifiedBadgeLine, RiWindowLine } from '@remixicon/react' +import { RiArrowRightSLine, RiBookOpenLine, RiBuildingLine, RiExternalLinkLine, RiGlobalLine, RiLockLine, RiPaintBrushLine, RiPaletteLine, RiSettings2Line, RiVerifiedBadgeLine, RiWindowLine } from '@remixicon/react' import { Trans } from 'react-i18next' import CopyFeedback from '@/app/components/base/copy-feedback' import Divider from '@/app/components/base/divider' @@ -72,7 +73,7 @@ const OPERATION_ICON_MAP: Record = { launch: RiExternalLinkLine, embedded: RiWindowLine, customize: RiPaintBrushLine, - settings: RiEqualizer2Line, + settings: RiPaletteLine, develop: RiBookOpenLine, } @@ -336,22 +337,25 @@ export const AppCardOperations = ({ }) => ( <> {operations.map(({ key, label, Icon, disabled, onClick }) => { + const shouldTruncate = key === 'customize' || key === 'settings' const buttonContent = ( - -
- -
{label}
+
+ +
+ {label}
- +
) if (key === 'launch' && launchConfigAction) { return ( -
+
*]:max-w-full [&>*]:min-w-0', + !shouldTruncate && 'shrink-0', + )} size="small" variant="ghost" key={key} + title={shouldTruncate ? label : undefined} onClick={onClick} disabled={disabled} > {buttonContent} ) + + if (disabled) { + return ( + + {actionButton} + + ) + } + + return actionButton })} ) diff --git a/web/app/components/app/overview/settings/__tests__/index.spec.tsx b/web/app/components/app/overview/settings/__tests__/index.spec.tsx index facebf0b91c..d11756260dd 100644 --- a/web/app/components/app/overview/settings/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/settings/__tests__/index.spec.tsx @@ -144,18 +144,14 @@ describe('SettingsModal', () => { vi.useRealTimers() }) - it('should render the modal and expose the expanded settings section', async () => { + it('should render the modal with all settings exposed by default', async () => { renderSettingsModal() expect(screen.getByText('appOverview.overview.appInfo.settings.title')).toBeInTheDocument() - const showMoreEntry = screen.getByText('appOverview.overview.appInfo.settings.more.entry') - fireEvent.click(showMoreEntry) - - await waitFor(() => { - expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toBeInTheDocument() - expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.copyRightPlaceholder')).toBeInTheDocument() - expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument() - }) + expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument() + expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toBeInTheDocument() + expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.copyRightPlaceholder')).toBeInTheDocument() + expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument() }) it('should notify the user when the name is empty', async () => { @@ -184,9 +180,8 @@ describe('SettingsModal', () => { expect(mockOnSave).not.toHaveBeenCalled() }) - it('should validate the privacy policy URL when advanced settings are open', async () => { + it('should validate the privacy policy URL', async () => { renderSettingsModal() - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) const privacyInput = screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder') fireEvent.change(privacyInput, { target: { value: 'ftp://invalid-url' } }) @@ -228,14 +223,10 @@ describe('SettingsModal', () => { expect(mockOnClose).toHaveBeenCalled() }) - it('should keep one show-more trigger while toggling the advanced section', () => { + it('should not render a show-more trigger', () => { renderSettingsModal() - expect(screen.getAllByText('appOverview.overview.appInfo.settings.more.entry')).toHaveLength(1) - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) - - expect(screen.getAllByText('appOverview.overview.appInfo.settings.more.entry')).toHaveLength(1) + expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument() expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument() }) @@ -249,8 +240,6 @@ describe('SettingsModal', () => { onSave={mockOnSave} />, ) - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument() rerender( @@ -272,8 +261,8 @@ describe('SettingsModal', () => { />, ) - expect(screen.getByText('appOverview.overview.appInfo.settings.more.entry')).toBeInTheDocument() - expect(screen.queryByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).not.toBeInTheDocument() + expect(screen.queryByText('appOverview.overview.appInfo.settings.more.entry')).not.toBeInTheDocument() + expect(screen.getByPlaceholderText('appOverview.overview.appInfo.settings.more.privacyPolicyPlaceholder')).toBeInTheDocument() }) it('should reset the input placeholder when app info changes while open', () => { @@ -286,8 +275,6 @@ describe('SettingsModal', () => { onSave={mockOnSave} />, ) - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Ask me anything') rerender( @@ -305,8 +292,6 @@ describe('SettingsModal', () => { onSave={mockOnSave} />, ) - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) expect(screen.getByRole('textbox', { name: inputPlaceholderName })).toHaveValue('Updated prompt') }) @@ -324,8 +309,6 @@ describe('SettingsModal', () => { renderSettingsModal() - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) - const inputPlaceholder = screen.getByRole('textbox', { name: inputPlaceholderName }) expect(inputPlaceholder).toBeDisabled() expect(inputPlaceholder).toHaveValue('') @@ -354,8 +337,6 @@ describe('SettingsModal', () => { }) renderSettingsModal() - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) const inputPlaceholder = screen.getByRole('textbox', { name: inputPlaceholderName }) fireEvent.change(inputPlaceholder, { target: { value: 'Self-hosted prompt' } }) fireEvent.click(screen.getByText('common.operation.save')) @@ -382,8 +363,6 @@ describe('SettingsModal', () => { }) renderSettingsModal() - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) fireEvent.click((await screen.findAllByText('billing.upgradeBtn.encourageShort'))[0]!) expect(mockSetShowPricingModal).toHaveBeenCalled() @@ -402,8 +381,6 @@ describe('SettingsModal', () => { }) renderSettingsModal() - - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) await waitFor(() => { expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() }) @@ -424,8 +401,6 @@ describe('SettingsModal', () => { renderSettingsModal(imageAppInfo) - fireEvent.click(screen.getByText('appOverview.overview.appInfo.settings.more.entry')) - fireEvent.change(screen.getByDisplayValue('A description'), { target: { value: 'Updated description' }, }) diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 1a1a3a1a0cf..a9f406137af 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -4,7 +4,6 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconType, Language, SiteConfig } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' -import { CollapsiblePanel, CollapsibleRoot, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible' import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { FieldControl, FieldDescription, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' @@ -173,7 +172,6 @@ const SettingsModal: FC = ({ onClose, onSave, }) => { - const [isShowMore, setIsShowMore] = useState(false) const [inputPlaceholderFocused, setInputPlaceholderFocused] = useState(false) const { default_language } = appInfo.site const nextInputInfo = createInputInfo(appInfo) @@ -250,13 +248,11 @@ const SettingsModal: FC = ({ setInputInfo(nextInputInfo) setLanguage(default_language) setAppIcon(nextAppIcon) - setIsShowMore(false) setPreviousSettingsResetKey(settingsResetKey) } } const handleClose = () => { - setIsShowMore(false) onClose() } @@ -463,147 +459,134 @@ const SettingsModal: FC = ({
{t(`${prefixSettings}.workflow.showDesc`, { ns: 'appOverview' })} - - - -
-
{t(`${prefixSettings}.more.entry`, { ns: 'appOverview' })}
-

- {t(`${prefixSettings}.more.copyRightPlaceholder`, { ns: 'appOverview' })} - {' '} - & - {' '} - {t(`${prefixSettings}.more.privacyPolicyPlaceholder`, { ns: 'appOverview' })} -

-
-
- -
- {INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && ( -
-
-
-
{t(`${prefixSettings}.more.inputPlaceholder`, { ns: 'appOverview' })}
- {isCloudSandboxPlan && ( -
- - + +
+ {INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && ( +
+
+
+
{t(`${prefixSettings}.more.inputPlaceholder`, { ns: 'appOverview' })}
+ {isCloudSandboxPlan && ( +
+ +
-
-

{t(`${prefixSettings}.more.inputPlaceholderTip`, { ns: 'appOverview' })}

- {isCloudSandboxPlan - ? ( - - - - {t(`${prefixSettings}.more.inputPlaceholderTooltip`, { ns: 'appOverview' })} - - - ) - : inputPlaceholderField} - {!isCloudSandboxPlan && ( -
- {`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`} +
)}
- )} - {/* copyright */} -
-
-
-
{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}
- {/* upgrade button */} - {isCloudSandboxPlan && ( -
- - -
- )} -
- {webappCopyrightEnabled - ? ( - setInputInfo({ ...inputInfo, copyrightSwitchValue: v })} - /> - ) - : ( - - - setInputInfo({ ...inputInfo, copyrightSwitchValue: v })} - /> -
- )} - /> - - {t(`${prefixSettings}.more.copyrightTooltip`, { ns: 'appOverview' })} - - - )} +
+

{t(`${prefixSettings}.more.inputPlaceholderTip`, { ns: 'appOverview' })}

+ {isCloudSandboxPlan + ? ( + + + + {t(`${prefixSettings}.more.inputPlaceholderTooltip`, { ns: 'appOverview' })} + + + ) + : inputPlaceholderField} + {!isCloudSandboxPlan && ( +
+ {`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
-

{t(`${prefixSettings}.more.copyrightTip`, { ns: 'appOverview' })}

- {copyrightSwitchValue && ( - + )} +
+ )} + {/* copyright */} +
+
+
+
{t(`${prefixSettings}.more.copyright`, { ns: 'appOverview' })}
+ {/* upgrade button */} + {isCloudSandboxPlan && ( +
+ + +
)}
- {/* privacy policy */} -
-
{t(`${prefixSettings}.more.privacyPolicy`, { ns: 'appOverview' })}
-

- }} - /> -

- -
- {/* custom disclaimer */} -
-
{t(`${prefixSettings}.more.customDisclaimer`, { ns: 'appOverview' })}
-

{t(`${prefixSettings}.more.customDisclaimerTip`, { ns: 'appOverview' })}

-