mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
💄 style(settings): clean up settings page copy and entries (#15117)
This commit is contained in:
@@ -113,7 +113,7 @@
|
||||
"modeSelection.title3": "Tell me, so I can tailor it just for you~",
|
||||
"next": "Next",
|
||||
"proSettings.connectors.title": "Connect Your Favorite Tools",
|
||||
"proSettings.devMode.title": "Developer Mode",
|
||||
"proSettings.devMode.title": "Advanced tools",
|
||||
"proSettings.model.fixed": "Default model is preset to {{provider}}/{{model}} in this environment.",
|
||||
"proSettings.model.title": "Default Model Used by the Agent",
|
||||
"proSettings.title": "Configure Advanced Options in Advance",
|
||||
|
||||
@@ -577,8 +577,8 @@
|
||||
"settingChatAppearance.transitionMode.options.none.value": "None",
|
||||
"settingChatAppearance.transitionMode.options.smooth": "Smooth",
|
||||
"settingChatAppearance.transitionMode.title": "Transition Animation",
|
||||
"settingCommon.devMode.desc": "Enable to show developer-related features and options",
|
||||
"settingCommon.devMode.title": "Developer Mode",
|
||||
"settingCommon.devMode.desc": "Show technical details and manual controls for chats, models, and local tools. This does not change model responses.",
|
||||
"settingCommon.devMode.title": "Advanced tools",
|
||||
"settingCommon.lang.autoMode": "Follow System",
|
||||
"settingCommon.lang.title": "Language",
|
||||
"settingCommon.liteMode.desc": "Simplify the interface and hide advanced features",
|
||||
@@ -869,6 +869,8 @@
|
||||
"tab.addCustomMcp.desc": "Manually configure a custom MCP server",
|
||||
"tab.addCustomSkill": "Add",
|
||||
"tab.advanced": "Advanced",
|
||||
"tab.advanced.appUpdates.title": "App updates",
|
||||
"tab.advanced.toolsAndDiagnostics.title": "Tools and diagnostics",
|
||||
"tab.advanced.updateChannel.canary": "Canary",
|
||||
"tab.advanced.updateChannel.canaryDesc": "Triggered on every PR merge, multiple builds per day. Most unstable.",
|
||||
"tab.advanced.updateChannel.desc": "By default, get notifications for stable updates. The Canary channel receives pre-release builds that may be unstable for production work.",
|
||||
@@ -876,7 +878,7 @@
|
||||
"tab.advanced.updateChannel.nightlyDesc": "Automated daily builds with the latest changes.",
|
||||
"tab.advanced.updateChannel.stable": "Stable",
|
||||
"tab.advanced.updateChannel.stableDesc": "Production-ready releases.",
|
||||
"tab.advanced.updateChannel.title": "Update Channel",
|
||||
"tab.advanced.updateChannel.title": "Update channel",
|
||||
"tab.agent": "Agent",
|
||||
"tab.all": "All",
|
||||
"tab.apikey": "API Keys",
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
"modeSelection.title3": "告诉我,为你定制",
|
||||
"next": "下一步",
|
||||
"proSettings.connectors.title": "连接你常用的工具",
|
||||
"proSettings.devMode.title": "开发者模式",
|
||||
"proSettings.devMode.title": "高级工具",
|
||||
"proSettings.model.fixed": "默认模型在此环境中预设为 {{provider}}/{{model}}。",
|
||||
"proSettings.model.title": "助理默认模型",
|
||||
"proSettings.title": "先配置一些进阶选项",
|
||||
|
||||
@@ -577,8 +577,8 @@
|
||||
"settingChatAppearance.transitionMode.options.none.value": "无",
|
||||
"settingChatAppearance.transitionMode.options.smooth": "平滑",
|
||||
"settingChatAppearance.transitionMode.title": "过渡动画",
|
||||
"settingCommon.devMode.desc": "开启后将显示开发者相关的功能和选项",
|
||||
"settingCommon.devMode.title": "开发者模式",
|
||||
"settingCommon.devMode.desc": "显示对话、模型和本地工具的技术细节与手动控制项。不会改变模型回复方式。",
|
||||
"settingCommon.devMode.title": "高级工具",
|
||||
"settingCommon.lang.autoMode": "跟随系统",
|
||||
"settingCommon.lang.title": "语言",
|
||||
"settingCommon.liteMode.desc": "简化界面并隐藏高级功能",
|
||||
@@ -869,6 +869,8 @@
|
||||
"tab.addCustomMcp.desc": "手动配置自定义 MCP 服务器",
|
||||
"tab.addCustomSkill": "添加",
|
||||
"tab.advanced": "高级设置",
|
||||
"tab.advanced.appUpdates.title": "应用更新",
|
||||
"tab.advanced.toolsAndDiagnostics.title": "工具与诊断",
|
||||
"tab.advanced.updateChannel.canary": "Canary",
|
||||
"tab.advanced.updateChannel.canaryDesc": "每次 PR 合并触发构建,一天可能多次。最不稳定。",
|
||||
"tab.advanced.updateChannel.desc": "默认情况下,接收稳定更新的通知。Canary 渠道会接收可能不适合生产环境的预发布版本。",
|
||||
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
'modeSelection.title3': 'Tell me, so I can tailor it just for you~',
|
||||
'next': 'Next',
|
||||
'proSettings.connectors.title': 'Connect Your Favorite Tools',
|
||||
'proSettings.devMode.title': 'Developer Mode',
|
||||
'proSettings.devMode.title': 'Advanced tools',
|
||||
'proSettings.model.fixed':
|
||||
'Default model is preset to {{provider}}/{{model}} in this environment.',
|
||||
'proSettings.model.title': 'Default Model Used by the Agent',
|
||||
|
||||
@@ -655,8 +655,9 @@ export default {
|
||||
'settingChatAppearance.transitionMode.options.none.value': 'None',
|
||||
'settingChatAppearance.transitionMode.options.smooth': 'Smooth',
|
||||
'settingChatAppearance.transitionMode.title': 'Transition Animation',
|
||||
'settingCommon.devMode.desc': 'Enable to show developer-related features and options',
|
||||
'settingCommon.devMode.title': 'Developer Mode',
|
||||
'settingCommon.devMode.desc':
|
||||
'Show technical details and manual controls for chats, models, and local tools. This does not change model responses.',
|
||||
'settingCommon.devMode.title': 'Advanced tools',
|
||||
'settingCommon.lang.autoMode': 'Follow System',
|
||||
'settingCommon.lang.title': 'Language',
|
||||
'settingCommon.liteMode.desc': 'Simplify the interface and hide advanced features',
|
||||
@@ -1006,6 +1007,8 @@ When I am ___, I need ___
|
||||
'systemAgent.translation.title': 'Message Translation',
|
||||
'tab.about': 'About',
|
||||
'tab.advanced': 'Advanced',
|
||||
'tab.advanced.appUpdates.title': 'App updates',
|
||||
'tab.advanced.toolsAndDiagnostics.title': 'Tools and diagnostics',
|
||||
'tab.addAgentSkill': 'Add Agent Skill',
|
||||
'tab.advanced.updateChannel.canary': 'Canary',
|
||||
'tab.advanced.updateChannel.canaryDesc':
|
||||
@@ -1016,7 +1019,7 @@ When I am ___, I need ___
|
||||
'tab.advanced.updateChannel.nightlyDesc': 'Automated daily builds with the latest changes.',
|
||||
'tab.advanced.updateChannel.stable': 'Stable',
|
||||
'tab.advanced.updateChannel.stableDesc': 'Production-ready releases.',
|
||||
'tab.advanced.updateChannel.title': 'Update Channel',
|
||||
'tab.advanced.updateChannel.title': 'Update channel',
|
||||
'tab.addCustomMcp': 'Add Custom MCP Skill',
|
||||
'tab.addCustomMcp.desc': 'Manually configure a custom MCP server',
|
||||
'tab.addCustomSkill': 'Add',
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { initServerConfigStore, Provider } from '@/store/serverConfig/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import Page from './index';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: vi.fn(() => null),
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock('@lobechat/const', async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
isDesktop: true,
|
||||
}));
|
||||
|
||||
vi.mock('antd-style', () => ({
|
||||
createStaticStyles: () => ({
|
||||
labItem: 'lab-item',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@lobehub/ui', () => ({
|
||||
Form: ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
children: { children?: ReactNode; desc?: string; label: string }[];
|
||||
title: string;
|
||||
}[];
|
||||
}) => (
|
||||
<div>
|
||||
{items.map((group) => (
|
||||
<section key={group.title}>
|
||||
<h2>{group.title}</h2>
|
||||
{group.children.map((item) => (
|
||||
<div key={item.label}>
|
||||
<span>{item.label}</span>
|
||||
{item.children}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
Icon: () => null,
|
||||
Skeleton: () => <div>loading</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@lobehub/ui/base-ui', () => ({
|
||||
Select: () => <button />,
|
||||
Switch: () => <button />,
|
||||
}));
|
||||
|
||||
vi.mock('@/routes/(main)/settings/features/SettingHeader', () => ({
|
||||
default: ({ title }: { title: string }) => <h1>{title}</h1>,
|
||||
}));
|
||||
|
||||
vi.mock('@/services/electron/autoUpdate', () => ({
|
||||
autoUpdateService: {
|
||||
getUpdateChannel: vi.fn().mockResolvedValue('stable'),
|
||||
setUpdateChannel: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createWrapper = () => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<Provider createStore={() => initServerConfigStore({})}>{children}</Provider>
|
||||
);
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
const initialUserStoreState = useUserStore.getState();
|
||||
|
||||
afterEach(() => {
|
||||
useUserStore.setState(initialUserStoreState, true);
|
||||
});
|
||||
|
||||
describe('Advanced settings page', () => {
|
||||
it('uses distinct group titles for tools and app updates', () => {
|
||||
useUserStore.setState({
|
||||
isUserStateInit: true,
|
||||
setSettings: vi.fn(),
|
||||
updateLab: vi.fn(),
|
||||
});
|
||||
|
||||
render(<Page />, { wrapper: createWrapper() });
|
||||
|
||||
expect(screen.getByText('tab.advanced.toolsAndDiagnostics.title')).toBeDefined();
|
||||
expect(screen.getByText('tab.advanced.appUpdates.title')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -75,7 +75,7 @@ const Page = memo(() => {
|
||||
},
|
||||
],
|
||||
extra: loading && <Icon spin icon={Loader2Icon} size={16} style={{ opacity: 0.5 }} />,
|
||||
title: t('tab.advanced'),
|
||||
title: t('tab.advanced.toolsAndDiagnostics.title'),
|
||||
};
|
||||
|
||||
const channelOptions = [
|
||||
@@ -93,7 +93,7 @@ const Page = memo(() => {
|
||||
label: t('tab.advanced.updateChannel.title'),
|
||||
},
|
||||
],
|
||||
title: t('tab.advanced.updateChannel.title'),
|
||||
title: t('tab.advanced.appUpdates.title'),
|
||||
};
|
||||
|
||||
const labItems: FormItemProps[] = [
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import onboarding from '@/locales/default/onboarding';
|
||||
import setting from '@/locales/default/setting';
|
||||
|
||||
describe('settings copy', () => {
|
||||
it('describes Advanced tools without repeating Developer Mode wording', () => {
|
||||
expect(setting['settingCommon.devMode.title']).toBe('Advanced tools');
|
||||
expect(setting['settingCommon.devMode.desc']).toBe(
|
||||
'Show technical details and manual controls for chats, models, and local tools. This does not change model responses.',
|
||||
);
|
||||
expect(onboarding['proSettings.devMode.title']).toBe('Advanced tools');
|
||||
});
|
||||
|
||||
it('uses non-repeating Advanced page group titles', () => {
|
||||
expect(setting['tab.advanced.toolsAndDiagnostics.title']).toBe('Tools and diagnostics');
|
||||
expect(setting['tab.advanced.appUpdates.title']).toBe('App updates');
|
||||
expect(setting['tab.advanced.updateChannel.title']).toBe('Update channel');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { mapFeatureFlagsEnvToState } from '@/config/featureFlags';
|
||||
import { SettingsTabs } from '@/store/global/initialState';
|
||||
import { initServerConfigStore, Provider } from '@/store/serverConfig/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import { useCategory } from './useCategory';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: vi.fn(() => null),
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const createWrapper = (showProvider: boolean) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<Provider
|
||||
createStore={() =>
|
||||
initServerConfigStore({
|
||||
featureFlags: {
|
||||
...mapFeatureFlagsEnvToState({
|
||||
provider_settings: true,
|
||||
}),
|
||||
showProvider,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
const getItemKeys = () => {
|
||||
const { result } = renderHook(() => useCategory(), {
|
||||
wrapper: createWrapper(true),
|
||||
});
|
||||
|
||||
return result.current.flatMap((group) => group.items.map((item) => item.key));
|
||||
};
|
||||
|
||||
const initialUserStoreState = useUserStore.getState();
|
||||
|
||||
afterEach(() => {
|
||||
useUserStore.setState(initialUserStoreState, true);
|
||||
});
|
||||
|
||||
describe('settings useCategory', () => {
|
||||
it('keeps Provider visible when provider settings are enabled', () => {
|
||||
expect(getItemKeys()).toContain(SettingsTabs.Provider);
|
||||
});
|
||||
|
||||
it('hides Provider when provider settings are disabled', () => {
|
||||
const { result } = renderHook(() => useCategory(), {
|
||||
wrapper: createWrapper(false),
|
||||
});
|
||||
|
||||
const keys = result.current.flatMap((group) => group.items.map((item) => item.key));
|
||||
|
||||
expect(keys).not.toContain(SettingsTabs.Provider);
|
||||
});
|
||||
});
|
||||
@@ -61,7 +61,7 @@ export const useCategory = () => {
|
||||
const { t: tAuth } = useTranslation('auth');
|
||||
const { t: tSubscription } = useTranslation('subscription');
|
||||
const mobile = useServerConfigStore((s) => s.isMobile);
|
||||
const { hideDocs, showApiKeyManage } = useServerConfigStore(featureFlagsSelectors);
|
||||
const { hideDocs, showApiKeyManage, showProvider } = useServerConfigStore(featureFlagsSelectors);
|
||||
const [avatar, username] = useUserStore((s) => [
|
||||
userProfileSelectors.userAvatar(s),
|
||||
userProfileSelectors.nickName(s),
|
||||
@@ -134,7 +134,9 @@ export const useCategory = () => {
|
||||
|
||||
// Agent group
|
||||
const agentItems: CategoryItem[] = [
|
||||
(!enableBusinessFeatures || isDevMode) && {
|
||||
// Provider settings should not depend on Advanced tools: new users may need
|
||||
// non-LobeHub providers, and desktop users often bring their own API keys.
|
||||
showProvider && {
|
||||
icon: Brain,
|
||||
key: SettingsTabs.Provider,
|
||||
label: t('tab.provider'),
|
||||
@@ -226,6 +228,7 @@ export const useCategory = () => {
|
||||
hideDocs,
|
||||
mobile,
|
||||
showApiKeyManage,
|
||||
showProvider,
|
||||
isDevMode,
|
||||
avatarUrl,
|
||||
username,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { mapFeatureFlagsEnvToState } from '@/config/featureFlags';
|
||||
import { initServerConfigStore, Provider } from '@/store/serverConfig/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import AdvancedActions from './Advanced';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: vi.fn(() => null),
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@lobehub/ui', () => ({
|
||||
Button: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => (
|
||||
<button onClick={onClick}>{children}</button>
|
||||
),
|
||||
Form: ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
children: { children?: ReactNode; desc?: string; label: string }[];
|
||||
title: string;
|
||||
}[];
|
||||
}) => (
|
||||
<div>
|
||||
{items.map((group) => (
|
||||
<section key={group.title}>
|
||||
<h2>{group.title}</h2>
|
||||
{group.children.map((item) => (
|
||||
<div key={item.label}>
|
||||
<span>{item.label}</span>
|
||||
{item.desc && <span>{item.desc}</span>}
|
||||
{item.children}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
Icon: () => null,
|
||||
ShikiLobeTheme: {},
|
||||
}));
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
App: {
|
||||
useApp: () => ({
|
||||
message: { success: vi.fn() },
|
||||
modal: { confirm: vi.fn() },
|
||||
}),
|
||||
},
|
||||
Switch: ({ checked, onChange }: { checked?: boolean; onChange?: (checked: boolean) => void }) => (
|
||||
<button
|
||||
aria-checked={checked}
|
||||
role="switch"
|
||||
onClick={() => {
|
||||
onChange?.(!checked);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/business/client/features/AccountDeletion', () => ({
|
||||
default: () => <div />,
|
||||
}));
|
||||
|
||||
vi.mock('@/features/DataImporter', () => ({
|
||||
default: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock('@/services/config', () => ({
|
||||
configService: {
|
||||
exportAll: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const createWrapper = (hideDocs: boolean) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<Provider
|
||||
createStore={() =>
|
||||
initServerConfigStore({
|
||||
featureFlags: {
|
||||
...mapFeatureFlagsEnvToState({
|
||||
commercial_hide_docs: false,
|
||||
}),
|
||||
hideDocs,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
const initialUserStoreState = useUserStore.getState();
|
||||
|
||||
afterEach(() => {
|
||||
useUserStore.setState(initialUserStoreState, true);
|
||||
});
|
||||
|
||||
describe('AdvancedActions', () => {
|
||||
it('does not duplicate analytics when About settings are visible', () => {
|
||||
render(<AdvancedActions />, { wrapper: createWrapper(false) });
|
||||
|
||||
expect(screen.queryByText('analytics.title')).toBeNull();
|
||||
expect(screen.getByText('storage.actions.title')).toBeDefined();
|
||||
});
|
||||
|
||||
it('shows telemetry as a fallback when About settings are hidden', () => {
|
||||
const updateGeneralConfig = vi.fn();
|
||||
|
||||
useUserStore.setState({
|
||||
settings: { general: { telemetry: true } },
|
||||
updateGeneralConfig,
|
||||
});
|
||||
|
||||
render(<AdvancedActions />, { wrapper: createWrapper(true) });
|
||||
|
||||
expect(screen.getByText('analytics.title')).toBeDefined();
|
||||
expect(screen.getByText('analytics.telemetry.title')).toBeDefined();
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'));
|
||||
|
||||
expect(updateGeneralConfig).toHaveBeenCalledWith({ telemetry: false });
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { BRANDING_NAME } from '@lobechat/business-const';
|
||||
import { DEFAULT_SETTINGS } from '@lobechat/config';
|
||||
import { type FormGroupItemType } from '@lobehub/ui';
|
||||
import { Button, Form, Icon } from '@lobehub/ui';
|
||||
import { App, Switch } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { HardDriveDownload, HardDriveUpload } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -17,17 +15,18 @@ import { configService } from '@/services/config';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useFileStore } from '@/store/file';
|
||||
import { useServerConfigStore } from '@/store/serverConfig';
|
||||
import { serverConfigSelectors } from '@/store/serverConfig/selectors';
|
||||
import { featureFlagsSelectors, serverConfigSelectors } from '@/store/serverConfig/selectors';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { settingsSelectors } from '@/store/user/selectors';
|
||||
import { userGeneralSettingsSelectors } from '@/store/user/selectors';
|
||||
|
||||
const AdvancedActions = () => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [form] = Form.useForm();
|
||||
const { message, modal } = App.useApp();
|
||||
const { hideDocs } = useServerConfigStore(featureFlagsSelectors);
|
||||
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
||||
const checked = useUserStore(userGeneralSettingsSelectors.telemetry);
|
||||
const [clearSessions, clearSessionGroups] = useSessionStore((s) => [
|
||||
s.clearSessions,
|
||||
s.clearSessionGroups,
|
||||
@@ -38,8 +37,8 @@ const AdvancedActions = () => {
|
||||
]);
|
||||
const [removeAllFiles] = useFileStore((s) => [s.removeAllFiles]);
|
||||
const removeAllPlugins = useToolStore((s) => s.removeAllPlugins);
|
||||
const settings = useUserStore(settingsSelectors.currentSettings, isEqual);
|
||||
const [setSettings, resetSettings] = useUserStore((s) => [s.setSettings, s.resetSettings]);
|
||||
const resetSettings = useUserStore((s) => s.resetSettings);
|
||||
const updateGeneralConfig = useUserStore((s) => s.updateGeneralConfig);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
modal.confirm({
|
||||
@@ -59,7 +58,17 @@ const AdvancedActions = () => {
|
||||
},
|
||||
title: t('danger.clear.confirm'),
|
||||
});
|
||||
}, []);
|
||||
}, [
|
||||
clearAllMessages,
|
||||
clearSessionGroups,
|
||||
clearSessions,
|
||||
clearTopics,
|
||||
message,
|
||||
modal,
|
||||
removeAllFiles,
|
||||
removeAllPlugins,
|
||||
t,
|
||||
]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
modal.confirm({
|
||||
@@ -67,26 +76,11 @@ const AdvancedActions = () => {
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
resetSettings();
|
||||
form.setFieldsValue(DEFAULT_SETTINGS);
|
||||
message.success(t('danger.reset.success'));
|
||||
},
|
||||
title: t('danger.reset.confirm'),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const analytics: FormGroupItemType = {
|
||||
children: [
|
||||
{
|
||||
children: <Switch />,
|
||||
desc: t('analytics.telemetry.desc', { appName: BRANDING_NAME }),
|
||||
label: t('analytics.telemetry.title'),
|
||||
minWidth: undefined,
|
||||
name: ['general', 'telemetry'],
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
],
|
||||
title: t('analytics.title'),
|
||||
};
|
||||
}, [message, modal, resetSettings, t]);
|
||||
|
||||
const renderExportButtonFormItem = () => {
|
||||
return {
|
||||
@@ -146,16 +140,34 @@ const AdvancedActions = () => {
|
||||
],
|
||||
title: t('storage.actions.title'),
|
||||
};
|
||||
|
||||
const analytics: FormGroupItemType = {
|
||||
children: [
|
||||
{
|
||||
children: (
|
||||
<Switch
|
||||
checked={!!checked}
|
||||
onChange={(value) => {
|
||||
updateGeneralConfig({ telemetry: value });
|
||||
}}
|
||||
/>
|
||||
),
|
||||
desc: t('analytics.telemetry.desc', { appName: BRANDING_NAME }),
|
||||
label: t('analytics.telemetry.title'),
|
||||
minWidth: undefined,
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
],
|
||||
title: t('analytics.title'),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
collapsible={false}
|
||||
form={form}
|
||||
initialValues={settings}
|
||||
items={[analytics, system]}
|
||||
items={hideDocs ? [analytics, system] : [system]}
|
||||
itemsType={'group'}
|
||||
variant={'filled'}
|
||||
onValuesChange={setSettings}
|
||||
{...FORM_STYLE}
|
||||
/>
|
||||
{enableBusinessFeatures && <AccountDeletion />}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { mapFeatureFlagsEnvToState } from '@/config/featureFlags';
|
||||
import { SettingsTabs } from '@/store/global/initialState';
|
||||
import { initServerConfigStore, Provider } from '@/store/serverConfig/store';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import { useCategory } from './useCategory';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: vi.fn(() => null),
|
||||
removeItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const navigate = vi.fn();
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => navigate,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
const createWrapper = (showProvider: boolean) => {
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<Provider
|
||||
createStore={() =>
|
||||
initServerConfigStore({
|
||||
featureFlags: {
|
||||
...mapFeatureFlagsEnvToState({
|
||||
provider_settings: true,
|
||||
}),
|
||||
showProvider,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
|
||||
const initialUserStoreState = useUserStore.getState();
|
||||
|
||||
afterEach(() => {
|
||||
navigate.mockReset();
|
||||
useUserStore.setState(initialUserStoreState, true);
|
||||
});
|
||||
|
||||
describe('mobile settings useCategory', () => {
|
||||
it('keeps Provider visible and routes to the provider list when provider settings are enabled', () => {
|
||||
const { result } = renderHook(() => useCategory(), {
|
||||
wrapper: createWrapper(true),
|
||||
});
|
||||
|
||||
const provider = result.current
|
||||
.flatMap((group) => group.items)
|
||||
.find((item) => item.key === SettingsTabs.Provider);
|
||||
|
||||
expect(provider).toBeDefined();
|
||||
|
||||
provider?.onClick?.();
|
||||
|
||||
expect(navigate).toHaveBeenCalledWith('/settings/provider/all');
|
||||
});
|
||||
|
||||
it('hides Provider when provider settings are disabled', () => {
|
||||
const { result } = renderHook(() => useCategory(), {
|
||||
wrapper: createWrapper(false),
|
||||
});
|
||||
|
||||
const keys = result.current.flatMap((group) => group.items.map((item) => item.key));
|
||||
|
||||
expect(keys).not.toContain(SettingsTabs.Provider);
|
||||
});
|
||||
});
|
||||
@@ -50,7 +50,7 @@ export interface CategoryGroup {
|
||||
export const useCategory = (): CategoryGroup[] => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation(['setting', 'auth', 'subscription']);
|
||||
const { hideDocs, showApiKeyManage } = useServerConfigStore(featureFlagsSelectors);
|
||||
const { hideDocs, showApiKeyManage, showProvider } = useServerConfigStore(featureFlagsSelectors);
|
||||
const enableBusinessFeatures = useServerConfigStore(serverConfigSelectors.enableBusinessFeatures);
|
||||
const isDevMode = useUserStore((s) => userGeneralSettingsSelectors.config(s).isDevMode);
|
||||
|
||||
@@ -100,7 +100,9 @@ export const useCategory = (): CategoryGroup[] => {
|
||||
: [];
|
||||
|
||||
const agent: CategoryItem[] = [
|
||||
(!enableBusinessFeatures || isDevMode) &&
|
||||
// Provider settings should not depend on Advanced tools: new users may need
|
||||
// non-LobeHub providers, and desktop users often bring their own API keys.
|
||||
showProvider &&
|
||||
makeItem({ icon: Brain, key: SettingsTabs.Provider, label: t('setting:tab.provider') }),
|
||||
makeItem({
|
||||
icon: Sparkles,
|
||||
@@ -136,5 +138,5 @@ export const useCategory = (): CategoryGroup[] => {
|
||||
{ items: agent, key: SettingsGroupKey.Agent, title: t('setting:group.aiConfig') },
|
||||
{ items: system, key: SettingsGroupKey.System, title: t('setting:group.system') },
|
||||
].filter((group) => group.items.length > 0);
|
||||
}, [t, enableBusinessFeatures, hideDocs, showApiKeyManage, isDevMode, navigate]);
|
||||
}, [t, enableBusinessFeatures, hideDocs, showApiKeyManage, showProvider, isDevMode, navigate]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user