mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
feat(console): page settings (#612)
This commit is contained in:
parent
e47899ae5c
commit
96848e6b0f
9 changed files with 198 additions and 12 deletions
|
@ -18,6 +18,7 @@ import Callback from './pages/Callback';
|
|||
import ConnectorDetails from './pages/ConnectorDetails';
|
||||
import Connectors from './pages/Connectors';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Settings from './pages/Settings';
|
||||
import SignInExperience from './pages/SignInExperience';
|
||||
import UserDetails from './pages/UserDetails';
|
||||
import Users from './pages/Users';
|
||||
|
@ -69,6 +70,7 @@ const Main = () => {
|
|||
<Route index element={<Navigate to="experience" />} />
|
||||
<Route path=":tab" element={<SignInExperience />} />
|
||||
</Route>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</SWRConfig>
|
||||
|
|
|
@ -31,7 +31,11 @@ const Sidebar = () => {
|
|||
</Section>
|
||||
))}
|
||||
<div className={styles.spacer} />
|
||||
<Item titleKey="settings" icon={<Gear />} />
|
||||
<Item
|
||||
titleKey="settings"
|
||||
icon={<Gear />}
|
||||
isActive={location.pathname.startsWith(getPath('settings'))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
128
packages/console/src/pages/Settings/index.tsx
Normal file
128
packages/console/src/pages/Settings/index.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
import { AppearanceMode, Setting } from '@logto/schemas';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import FormField from '@/components/FormField';
|
||||
import Select from '@/components/Select';
|
||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
|
||||
const Settings = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data, error, mutate } = useSWR<Setting, RequestError>('/api/settings');
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
register,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<Setting>();
|
||||
const api = useApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset(data);
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
if (!data || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedData = await api
|
||||
.patch('/api/settings', {
|
||||
json: {
|
||||
...formData,
|
||||
},
|
||||
})
|
||||
.json<Setting>();
|
||||
void mutate(updatedData);
|
||||
toast.success(t('settings.saved'));
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className={detailsStyles.container}>
|
||||
<CardTitle title="settings.title" subtitle="settings.description" />
|
||||
<TabNav>
|
||||
<TabNavLink href="/settings">{t('settings.tabs.general')}</TabNavLink>
|
||||
</TabNav>
|
||||
{!data && !error && <div>loading</div>}
|
||||
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||
{data && (
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormField title="admin_console.settings.custom_domain">
|
||||
<TextInput {...register('customDomain')} />
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.settings.language">
|
||||
<Controller
|
||||
name="adminConsole.language"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
value: Language.English,
|
||||
title: t('settings.language_english'),
|
||||
},
|
||||
{
|
||||
value: Language.Chinese,
|
||||
title: t('settings.language_chinese'),
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField isRequired title="admin_console.settings.appearance">
|
||||
<Controller
|
||||
name="adminConsole.appearanceMode"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
value: AppearanceMode.SyncWithSystem,
|
||||
title: t('settings.appearance_system'),
|
||||
},
|
||||
{
|
||||
value: AppearanceMode.LightMode,
|
||||
title: t('settings.appearance_light'),
|
||||
},
|
||||
{
|
||||
value: AppearanceMode.DarkMode,
|
||||
title: t('settings.appearance_dark'),
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<div className={detailsStyles.footer}>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
title="general.save_changes"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
|
@ -1,4 +1,6 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
import {
|
||||
AppearanceMode,
|
||||
Application,
|
||||
ApplicationType,
|
||||
Passcode,
|
||||
|
@ -44,7 +46,10 @@ export const mockRole: Role = {
|
|||
export const mockSetting: Setting = {
|
||||
id: 'foo setting',
|
||||
customDomain: 'mock-logto.dev',
|
||||
adminConsole: {},
|
||||
adminConsole: {
|
||||
language: Language.English,
|
||||
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPasscode: Passcode = {
|
||||
|
|
|
@ -27,7 +27,7 @@ describe('settings routes', () => {
|
|||
|
||||
it('PATCH /settings', async () => {
|
||||
const customDomain = 'silverhand-logto.io';
|
||||
const adminConsole = {};
|
||||
const { adminConsole } = mockSetting;
|
||||
|
||||
const response = await roleRequester.patch('/settings').send({
|
||||
customDomain,
|
||||
|
|
|
@ -402,6 +402,22 @@ const translation = {
|
|||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: 'settings',
|
||||
description: 'Global settings and others',
|
||||
tabs: {
|
||||
general: 'General',
|
||||
},
|
||||
custom_domain: 'Custom domain',
|
||||
language: 'Language',
|
||||
language_english: 'English',
|
||||
language_chinese: 'Chinese',
|
||||
appearance: 'Appearance',
|
||||
appearance_system: 'Sync with system',
|
||||
appearance_light: 'Light mode',
|
||||
appearance_dark: 'Dark mode',
|
||||
saved: 'Saved!',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -398,6 +398,22 @@ const translation = {
|
|||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
description: '全局设置',
|
||||
tabs: {
|
||||
general: '通用',
|
||||
},
|
||||
custom_domain: '自定义域名',
|
||||
language: '语言',
|
||||
language_english: '英语',
|
||||
language_chinese: '中文',
|
||||
appearance: '外观',
|
||||
appearance_system: '跟随系统',
|
||||
appearance_light: '浅色模式',
|
||||
appearance_dark: '深色模式',
|
||||
saved: '已保存',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -67,14 +67,6 @@ export const identitiesGuard = z.record(identityGuard);
|
|||
export type Identity = z.infer<typeof identityGuard>;
|
||||
export type Identities = z.infer<typeof identitiesGuard>;
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
|
||||
export const adminConsoleConfigGuard = z.object({});
|
||||
|
||||
export type AdminConsoleConfig = z.infer<typeof adminConsoleConfigGuard>;
|
||||
|
||||
/**
|
||||
* SignIn Experiences
|
||||
*/
|
||||
|
@ -137,3 +129,20 @@ export type SignInMethods = z.infer<typeof signInMethodsGuard>;
|
|||
export const connectorIdsGuard = z.string().array();
|
||||
|
||||
export type ConnectorIds = z.infer<typeof connectorIdsGuard>;
|
||||
|
||||
/**
|
||||
* Settings
|
||||
*/
|
||||
|
||||
export enum AppearanceMode {
|
||||
SyncWithSystem = 'SyncWithSystem',
|
||||
LightMode = 'LightMode',
|
||||
DarkMode = 'DarkMode',
|
||||
}
|
||||
|
||||
export const adminConsoleConfigGuard = z.object({
|
||||
language: z.nativeEnum(Language),
|
||||
appearanceMode: z.nativeEnum(AppearanceMode),
|
||||
});
|
||||
|
||||
export type AdminConsoleConfig = z.infer<typeof adminConsoleConfigGuard>;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { Language } from '@logto/phrases';
|
||||
|
||||
import { CreateSetting } from '../db-entries';
|
||||
import { AppearanceMode } from '../foundations';
|
||||
|
||||
export const defaultSettingId = 'default';
|
||||
|
||||
|
@ -6,5 +9,8 @@ export const createDefaultSetting = (customDomain: string): Readonly<CreateSetti
|
|||
Object.freeze({
|
||||
id: defaultSettingId,
|
||||
customDomain,
|
||||
adminConsole: {},
|
||||
adminConsole: {
|
||||
language: Language.English,
|
||||
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue