Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
- name: Run verification-code countdown lifecycle contract
run: yarn run test:verification-code-countdown

- name: Run settings layout contract
run: yarn run test:settings-layout

- name: Build Community web client
run: yarn run build:web:community --app_version=5.3.0

Expand Down
1 change: 1 addition & 0 deletions chat2db-community-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"test:search-result-tab-selection": "tsx src/blocks/SearchResult/tabSelection.test.ts",
"test:saved-console-tree-refresh": "tsx src/store/tree/savedConsoleTreeRefresh.test.ts",
"test:saved-console-lifecycle": "tsx src/store/workspace/utils/savedConsoleLifecycle.test.ts",
"test:settings-layout": "tsx src/blocks/Setting/navigation.test.ts && tsx src/blocks/Setting/search.test.ts && tsx src/blocks/Setting/BaseSetting/model.test.ts && tsx src/blocks/Setting/settingsLayout.test.ts",
"test:sql-execution-log": "tsx src/service/sqlExecutionLog.test.ts",
"test:sql-in-clipboard": "tsx src/utils/sqlInClipboard.test.ts && tsx src/components/SQLEditor/helper/sqlInsertValueDefaults.test.ts",
"test:tree-title-highlight": "tsx src/blocks/NewTree/components/TitleRender/highlightSearchText.test.ts",
Expand Down
8 changes: 4 additions & 4 deletions chat2db-community-client/scripts/i18n-source-hashes.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
"license.ts": "6aff8c38b1a7475264aa1e16e88d9b9731ecef112af70fe53edab449788ea788",
"login.ts": "d6b15f2b4db713711c093108372027ad65899aec6dd963d3f954231a9ce5b890",
"menu.ts": "1f9adabb7465ed545bdaa57fd2a0c10913e04979e710f65c6822f295c4d3e6ed",
"monaco.ts": "0c7f14a176a65e1e534d68bead392907430b943b80edd1ce173a1ddfcc50ab4f",
"monaco.ts": "da4d9ea1a29de505f94ca9c49d125805fbef1c3132c7aa0f49f1c278b8cb1e93",
"notification.ts": "500751da2f02391622f074448eb46611fd41da2e5530d6c7d65f17a1382bd236",
"plugin.ts": "01f15b6342cc5b29899ee4144d67b04bb4bc02c6bb10779806be322dc5e9a2bc",
"price.ts": "26e35cb96368585f83a29adbb14e300da9b7f5278a4320063e81a1d1dd0adee9",
"redis.ts": "a73e4fca6d783031370060e20bb1bbba13f663a0773cd031e1744be1cb4e93af",
"setting.ts": "0cbee33ef6464e82b35af4fc22bbe15fceb34a5b395fc582bac50ee77424f38c",
"setting.ts": "447e759e9842d790e80e6d5680032bf150ec259c6d48daa87458277a78a9ba1f",
"spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980",
"sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9",
"stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4",
Expand All @@ -43,12 +43,12 @@
"license.ts": "6aff8c38b1a7475264aa1e16e88d9b9731ecef112af70fe53edab449788ea788",
"login.ts": "d6b15f2b4db713711c093108372027ad65899aec6dd963d3f954231a9ce5b890",
"menu.ts": "1f9adabb7465ed545bdaa57fd2a0c10913e04979e710f65c6822f295c4d3e6ed",
"monaco.ts": "0c7f14a176a65e1e534d68bead392907430b943b80edd1ce173a1ddfcc50ab4f",
"monaco.ts": "da4d9ea1a29de505f94ca9c49d125805fbef1c3132c7aa0f49f1c278b8cb1e93",
"notification.ts": "500751da2f02391622f074448eb46611fd41da2e5530d6c7d65f17a1382bd236",
"plugin.ts": "01f15b6342cc5b29899ee4144d67b04bb4bc02c6bb10779806be322dc5e9a2bc",
"price.ts": "26e35cb96368585f83a29adbb14e300da9b7f5278a4320063e81a1d1dd0adee9",
"redis.ts": "a73e4fca6d783031370060e20bb1bbba13f663a0773cd031e1744be1cb4e93af",
"setting.ts": "0cbee33ef6464e82b35af4fc22bbe15fceb34a5b395fc582bac50ee77424f38c",
"setting.ts": "447e759e9842d790e80e6d5680032bf150ec259c6d48daa87458277a78a9ba1f",
"spaceSetting.ts": "defdf3138e5e1782f0282760bcef4ce1b8550cc80b03052fea468d97165cb980",
"sqlEditor.ts": "5c41e5f24d5ba78909795bcc681cabb8460cca075233b665dc4940afe82ce0c9",
"stream.ts": "8418dd25b0fd54b2ef2b15a2573edf0a4876efb4e69936ff82a157ea5cf42be4",
Expand Down
235 changes: 125 additions & 110 deletions chat2db-community-client/src/blocks/Setting/BaseSetting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,18 @@ import themeDarkDimmedImg from '@/assets/img/theme-dark-dimmed.png';
import themeDarkImg from '@/assets/img/theme-dark.png';
import themeLightImg from '@/assets/img/theme-light.png';
import { runtimeEditionConfig } from '@/constants/runtimeEdition';
import { LangType } from '@/constants/settings';
import type { LangType } from '@/constants/settings';
import i18n from '@/i18n';
import { useGlobalStore } from '@/store/global';
import { settingSelectors } from '@/store/global/selectors';
import { refreshPage } from '@/utils';
import { PrimaryColors, primaryColorsScales, PrimaryGradient, Swatches, ThemeAppearance } from '@chat2db/ui';
import { Form, Input, Select } from 'antd';
import { ChevronDown } from 'lucide-react';
import { PrimaryColors, primaryColorsScales, PrimaryGradient, ThemeAppearance } from '@chat2db/ui';
import { Input, Select } from 'antd';
import { Check, ChevronDown, Globe, Palette, Type } from 'lucide-react';
import { useMemo } from 'react';
import SettingSubsection from '../SettingSubsection';
import { getAvailableLanguageOptions, resolveCurrentLanguage } from './model';
import { useStyles } from './style';

const languageOptions = [
{ value: LangType.ZH_CN, label: '简体中文' },
{ value: LangType.EN_US, label: 'English' },
{ value: LangType.JA_JP, label: '日本語' },
{ value: LangType.ES_ES, label: 'Español' },
{ value: LangType.KO_KR, label: '한국어' },
];

const customFontSizeOptions = [
{ value: 12, label: '12px' },
{ value: 13, label: '13px' },
Expand Down Expand Up @@ -74,7 +66,6 @@ export default function BaseSetting() {
setAppearance: state.setAppearance,
isCN: state.appConfig.isCN,
setPrimaryColor: state.setPrimaryColor,
setNeutralColor: state.setNeutralColor,
setLanguage: state.setLanguage,
setCustomFont: state.setCustomFont,
setCustomFontSize: state.setCustomFontSize,
Expand All @@ -83,18 +74,12 @@ export default function BaseSetting() {

// If it is not a domestic version, Chinese will not be displayed.
const curLanguageOptions = useMemo(() => {
if (runtimeEditionConfig.languageRegionRestricted && !isCN) {
return languageOptions.filter((item) => item.value !== LangType.ZH_CN);
}
return languageOptions;
return getAvailableLanguageOptions(runtimeEditionConfig.languageRegionRestricted, isCN);
}, [isCN]);

// If it is not a domestic version, Chinese will not be displayed.
const curLanguage = useMemo(() => {
if (runtimeEditionConfig.languageRegionRestricted && !isCN && language === LangType.ZH_CN) {
return LangType.EN_US;
}
return language;
return resolveCurrentLanguage(language, runtimeEditionConfig.languageRegionRestricted, isCN);
}, [language, isCN]);

const isDark = appearance.includes('dark');
Expand All @@ -113,108 +98,138 @@ export default function BaseSetting() {
window.setTimeout(refreshPage, 0);
}

function changeThemeMode(item: any) {
setAppearance(item.code);
function changeThemeMode(code: ThemeAppearance) {
setAppearance(code);
}

function changePrimaryColor(item: any) {
setPrimaryColor(item);
}

// function changeNeutralColor(item: any) {
// setNeutralColor(item);
// }

return (
<div className={styles.baseSettingBox}>
<div>
<SettingSubsection
title={i18n('setting.title.backgroundColor')}
describe={i18n('setting.text.backgroundColorDescribe')}
/>
<div className={styles.backgroundList}>
{themeList.map((t) => {
return (
<div key={t.code} className={styles.themeItemBox}>
<div
className={cx(styles.themeBox, { [styles.activeThemeBox]: appearance == t.code })}
onClick={changeThemeMode.bind(null, t)}
style={{ backgroundImage: `url(${t.img})` }}
/>
<div className={styles.themeName}>{t.name}</div>
</div>
);
})}
<div className={styles.settingsList}>
<div className={styles.settingRow} data-setting-group="appearance" data-setting-search-id="basic.appearance">
<div className={styles.settingMeta}>
<Palette aria-hidden="true" className={styles.settingGroupIcon} size={18} strokeWidth={1.8} />
<div className={styles.settingMetaContent}>
<div className={styles.settingTitle} data-setting-search-title="true">
{i18n('setting.title.appearance')}
</div>
<div className={styles.settingDescription}>{i18n('setting.text.appearanceDescribe')}</div>
</div>
</div>
<div className={styles.settingStack}>
<div className={styles.controlBlock}>
<div className={styles.fieldLabel}>{i18n('setting.title.backgroundColor')}</div>
<div aria-label={i18n('setting.title.backgroundColor')} className={styles.backgroundList} role="group">
{themeList.map((t) => {
const isActive = appearance === t.code;
return (
<button
aria-pressed={isActive}
className={cx(styles.themeItemBox, { [styles.activeThemeItemBox]: isActive })}
key={t.code}
onClick={() => changeThemeMode(t.code)}
type="button"
>
<span
className={cx(styles.themeBox, { [styles.activeThemeBox]: isActive })}
style={{ backgroundImage: `url(${t.img})` }}
>
{isActive ? (
<span className={styles.themeCheck}>
<Check aria-hidden="true" size={13} strokeWidth={2.2} />
</span>
) : null}
</span>
<span className={styles.themeName}>{t.name}</span>
</button>
);
})}
</div>
</div>
<div aria-label={i18n('setting.title.themeColor')} className={styles.controlBlock} role="group">
<div className={styles.fieldLabel}>{i18n('setting.title.themeColor')}</div>
<div className={styles.swatchesBox}>
{primaryColorsSwatches.map((color) => {
const isActive = primaryColor?.label === color.label;
return (
<button
aria-label={color.label}
aria-pressed={isActive}
className={cx(styles.colorSwatch, { [styles.activeColorSwatch]: isActive })}
key={color.label}
onClick={() => changePrimaryColor(color)}
style={{ backgroundColor: color.value }}
title={color.label}
type="button"
>
{isActive ? <Check aria-hidden="true" size={14} strokeWidth={2.5} /> : null}
</button>
);
})}
</div>
</div>
</div>
</div>
<div>
<SettingSubsection title={i18n('setting.title.language')} describe={i18n('setting.title.languageDescribe')} />
<Select
value={curLanguage}
style={{ width: 140 }}
onChange={changeLang}
options={curLanguageOptions}
suffixIcon={<ChevronDown size={14} />}
/>
</div>
<div>
<SettingSubsection
title={i18n('setting.title.customFont')}
describe={i18n('setting.title.customFontDescribe')}
/>
<Form className={styles.customFontBox}>
{/* <Form.Item label={i18n('setting.title.customFont')} name="customFont"> */}
<Input
value={customFont}
style={{ width: 300 }}
onChange={(e) => {
setCustomFont(e.target.value);
}}
placeholder={i18n('setting.placeholder.customFont')}
/>
{/* </Form.Item>
<Form.Item label={i18n('setting.title.customFontSize')} name="customFontSize"> */}
<div className={styles.settingRow} data-setting-group="language" data-setting-search-id="basic.language">
<div className={styles.settingMeta}>
<Globe aria-hidden="true" className={styles.settingGroupIcon} size={18} strokeWidth={1.8} />
<div className={styles.settingMetaContent}>
<div className={styles.settingTitle} data-setting-search-title="true">
{i18n('setting.title.language')}
</div>
<div className={styles.settingDescription}>{i18n('setting.title.languageDescribe')}</div>
</div>
</div>
<div className={styles.settingControl}>
<Select
value={customFontSize}
placeholder={i18n('setting.title.customFontSizeDescribe')}
style={{ width: 200 }}
onChange={(e) => {
setCustomFontSize(e);
}}
options={customFontSizeOptions}
aria-label={i18n('setting.title.language')}
className={styles.languageSelect}
value={curLanguage}
onChange={changeLang}
options={curLanguageOptions}
suffixIcon={<ChevronDown size={14} />}
/>
{/* </Form.Item> */}
</Form>
</div>
<div>
<SettingSubsection
title={i18n('setting.title.themeColor')}
describe={i18n('setting.title.themeColorDescribe')}
/>
<Swatches
size={28}
gap={12}
activeColor={primaryColor?.label}
colors={primaryColorsSwatches}
onSelect={changePrimaryColor}
/>
</div>
</div>
{/* <div>
<SettingSubsection
title={i18n('setting.title.neutralColor')}
describe={i18n('setting.title.neutralColorDescribe')}
/>
<div className={styles.primaryColorList}>
<Swatches
size={28}
gap={12}
activeColor={neutralColor}
colors={neutralColorsSwatches}
onSelect={changeNeutralColor}
/>
<div className={styles.settingRow} data-setting-group="typography" data-setting-search-id="basic.typography">
<div className={styles.settingMeta}>
<Type aria-hidden="true" className={styles.settingGroupIcon} size={18} strokeWidth={1.8} />
<div className={styles.settingMetaContent}>
<div className={styles.settingTitle} data-setting-search-title="true">
{i18n('setting.title.interfaceFont')}
</div>
<div className={styles.settingDescription}>{i18n('setting.text.interfaceFontDescribe')}</div>
</div>
</div>
<div className={styles.fontControls}>
<label className={styles.fieldControl}>
<span className={styles.fieldLabel}>{i18n('setting.title.customFont')}</span>
<Input
aria-label={i18n('setting.title.customFont')}
value={customFont}
onChange={(e) => {
setCustomFont(e.target.value);
}}
placeholder={i18n('setting.placeholder.customFont')}
/>
</label>
<label className={styles.fieldControl}>
<span className={styles.fieldLabel}>{i18n('setting.title.customFontSize')}</span>
<Select
aria-label={i18n('setting.title.customFontSize')}
value={customFontSize}
placeholder={i18n('setting.title.customFontSizeDescribe')}
onChange={(value) => {
setCustomFontSize(value);
}}
options={customFontSizeOptions}
suffixIcon={<ChevronDown size={14} />}
/>
</label>
</div>
</div> */}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import assert from 'node:assert/strict';
import type { LangType } from '@/constants/settings';
import { getAvailableLanguageOptions, languageOptions, resolveCurrentLanguage } from './model';

assert.equal(
getAvailableLanguageOptions(false, false).length,
languageOptions.length,
'unrestricted editions expose every supported language',
);
assert.equal(
getAvailableLanguageOptions(true, false).some((item) => item.value === 'zh-CN'),
false,
'region-restricted non-CN editions hide Simplified Chinese',
);
assert.equal(
getAvailableLanguageOptions(true, true).some((item) => item.value === 'zh-CN'),
true,
'region-restricted CN editions keep Simplified Chinese available',
);
assert.equal(
resolveCurrentLanguage('zh-CN' as LangType, true, false),
'en-US',
'a persisted Simplified Chinese preference falls back to English when it is unavailable',
);
assert.equal(
resolveCurrentLanguage('ja-JP' as LangType, true, false),
'ja-JP',
'available persisted languages remain unchanged',
);

console.log('Base settings model tests passed.');
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { LangType } from '@/constants/settings';

const EN_US = 'en-US' as LangType;
const ZH_CN = 'zh-CN' as LangType;
const JA_JP = 'ja-JP' as LangType;
const ES_ES = 'es-ES' as LangType;
const KO_KR = 'ko-KR' as LangType;

export const languageOptions = [
{ value: ZH_CN, label: '简体中文' },
{ value: EN_US, label: 'English' },
{ value: JA_JP, label: '日本語' },
{ value: ES_ES, label: 'Español' },
{ value: KO_KR, label: '한국어' },
];

export function getAvailableLanguageOptions(languageRegionRestricted: boolean, isCN: boolean) {
if (languageRegionRestricted && !isCN) {
return languageOptions.filter((item) => item.value !== ZH_CN);
}
return languageOptions;
}

export function resolveCurrentLanguage(language: LangType, languageRegionRestricted: boolean, isCN: boolean) {
if (languageRegionRestricted && !isCN && language === ZH_CN) {
return EN_US;
}
return language;
}
Loading
Loading