0
Fork 0
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:
Wang Sijie 2022-04-24 11:46:35 +08:00 committed by GitHub
parent e47899ae5c
commit 96848e6b0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 198 additions and 12 deletions

View file

@ -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>

View file

@ -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>
);
};

View 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;

View file

@ -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 = {

View file

@ -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,

View file

@ -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!',
},
},
};

View file

@ -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: '已保存',
},
},
};

View file

@ -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>;

View file

@ -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,
},
});