mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -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 ConnectorDetails from './pages/ConnectorDetails';
|
||||||
import Connectors from './pages/Connectors';
|
import Connectors from './pages/Connectors';
|
||||||
import NotFound from './pages/NotFound';
|
import NotFound from './pages/NotFound';
|
||||||
|
import Settings from './pages/Settings';
|
||||||
import SignInExperience from './pages/SignInExperience';
|
import SignInExperience from './pages/SignInExperience';
|
||||||
import UserDetails from './pages/UserDetails';
|
import UserDetails from './pages/UserDetails';
|
||||||
import Users from './pages/Users';
|
import Users from './pages/Users';
|
||||||
|
@ -69,6 +70,7 @@ const Main = () => {
|
||||||
<Route index element={<Navigate to="experience" />} />
|
<Route index element={<Navigate to="experience" />} />
|
||||||
<Route path=":tab" element={<SignInExperience />} />
|
<Route path=":tab" element={<SignInExperience />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</SWRConfig>
|
</SWRConfig>
|
||||||
|
|
|
@ -31,7 +31,11 @@ const Sidebar = () => {
|
||||||
</Section>
|
</Section>
|
||||||
))}
|
))}
|
||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<Item titleKey="settings" icon={<Gear />} />
|
<Item
|
||||||
|
titleKey="settings"
|
||||||
|
icon={<Gear />}
|
||||||
|
isActive={location.pathname.startsWith(getPath('settings'))}
|
||||||
|
/>
|
||||||
</div>
|
</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 {
|
import {
|
||||||
|
AppearanceMode,
|
||||||
Application,
|
Application,
|
||||||
ApplicationType,
|
ApplicationType,
|
||||||
Passcode,
|
Passcode,
|
||||||
|
@ -44,7 +46,10 @@ export const mockRole: Role = {
|
||||||
export const mockSetting: Setting = {
|
export const mockSetting: Setting = {
|
||||||
id: 'foo setting',
|
id: 'foo setting',
|
||||||
customDomain: 'mock-logto.dev',
|
customDomain: 'mock-logto.dev',
|
||||||
adminConsole: {},
|
adminConsole: {
|
||||||
|
language: Language.English,
|
||||||
|
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mockPasscode: Passcode = {
|
export const mockPasscode: Passcode = {
|
||||||
|
|
|
@ -27,7 +27,7 @@ describe('settings routes', () => {
|
||||||
|
|
||||||
it('PATCH /settings', async () => {
|
it('PATCH /settings', async () => {
|
||||||
const customDomain = 'silverhand-logto.io';
|
const customDomain = 'silverhand-logto.io';
|
||||||
const adminConsole = {};
|
const { adminConsole } = mockSetting;
|
||||||
|
|
||||||
const response = await roleRequester.patch('/settings').send({
|
const response = await roleRequester.patch('/settings').send({
|
||||||
customDomain,
|
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 Identity = z.infer<typeof identityGuard>;
|
||||||
export type Identities = z.infer<typeof identitiesGuard>;
|
export type Identities = z.infer<typeof identitiesGuard>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const adminConsoleConfigGuard = z.object({});
|
|
||||||
|
|
||||||
export type AdminConsoleConfig = z.infer<typeof adminConsoleConfigGuard>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SignIn Experiences
|
* SignIn Experiences
|
||||||
*/
|
*/
|
||||||
|
@ -137,3 +129,20 @@ export type SignInMethods = z.infer<typeof signInMethodsGuard>;
|
||||||
export const connectorIdsGuard = z.string().array();
|
export const connectorIdsGuard = z.string().array();
|
||||||
|
|
||||||
export type ConnectorIds = z.infer<typeof connectorIdsGuard>;
|
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 { CreateSetting } from '../db-entries';
|
||||||
|
import { AppearanceMode } from '../foundations';
|
||||||
|
|
||||||
export const defaultSettingId = 'default';
|
export const defaultSettingId = 'default';
|
||||||
|
|
||||||
|
@ -6,5 +9,8 @@ export const createDefaultSetting = (customDomain: string): Readonly<CreateSetti
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
id: defaultSettingId,
|
id: defaultSettingId,
|
||||||
customDomain,
|
customDomain,
|
||||||
adminConsole: {},
|
adminConsole: {
|
||||||
|
language: Language.English,
|
||||||
|
appearanceMode: AppearanceMode.SyncWithSystem,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue