mirror of https://gitee.com/answerdev/answer.git
commit
5f113285a1
|
@ -322,6 +322,7 @@ ui:
|
||||||
upgrade: Answer Upgrade
|
upgrade: Answer Upgrade
|
||||||
maintenance: Website Maintenance
|
maintenance: Website Maintenance
|
||||||
users: Users
|
users: Users
|
||||||
|
oauth_callback: Processing
|
||||||
http_404: HTTP Error 404
|
http_404: HTTP Error 404
|
||||||
http_50X: HTTP Error 500
|
http_50X: HTTP Error 500
|
||||||
http_403: HTTP Error 403
|
http_403: HTTP Error 403
|
||||||
|
@ -658,7 +659,6 @@ ui:
|
||||||
msg:
|
msg:
|
||||||
empty: Cannot be empty.
|
empty: Cannot be empty.
|
||||||
login:
|
login:
|
||||||
page_title: Welcome to {{site_name}}
|
|
||||||
login_to_continue: Log in to continue
|
login_to_continue: Log in to continue
|
||||||
info_sign: Don't have an account? <1>Sign up</1>
|
info_sign: Don't have an account? <1>Sign up</1>
|
||||||
info_login: Already have an account? <1>Log in</1>
|
info_login: Already have an account? <1>Log in</1>
|
||||||
|
@ -669,6 +669,7 @@ ui:
|
||||||
msg:
|
msg:
|
||||||
empty: Name cannot be empty.
|
empty: Name cannot be empty.
|
||||||
range: Name up to 30 characters.
|
range: Name up to 30 characters.
|
||||||
|
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||||
email:
|
email:
|
||||||
label: Email
|
label: Email
|
||||||
msg:
|
msg:
|
||||||
|
@ -689,7 +690,6 @@ ui:
|
||||||
msg:
|
msg:
|
||||||
empty: Email cannot be empty.
|
empty: Email cannot be empty.
|
||||||
change_email:
|
change_email:
|
||||||
page_title: Welcome to {{site_name}}
|
|
||||||
btn_cancel: Cancel
|
btn_cancel: Cancel
|
||||||
btn_update: Update email address
|
btn_update: Update email address
|
||||||
send_success: >-
|
send_success: >-
|
||||||
|
@ -699,6 +699,17 @@ ui:
|
||||||
label: New Email
|
label: New Email
|
||||||
msg:
|
msg:
|
||||||
empty: Email cannot be empty.
|
empty: Email cannot be empty.
|
||||||
|
oauth_bind_email:
|
||||||
|
subtitle: Add a recovery email to your account.
|
||||||
|
btn_update: Update email address
|
||||||
|
email:
|
||||||
|
label: Email
|
||||||
|
msg:
|
||||||
|
empty: Email cannot be empty.
|
||||||
|
modal_title: Email already existes.
|
||||||
|
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
|
||||||
|
modal_cancel: Change email
|
||||||
|
modal_confirm: Connect to the existing account
|
||||||
password_reset:
|
password_reset:
|
||||||
page_title: Password Reset
|
page_title: Password Reset
|
||||||
btn_name: Reset my password
|
btn_name: Reset my password
|
||||||
|
@ -770,6 +781,9 @@ ui:
|
||||||
email:
|
email:
|
||||||
label: Email
|
label: Email
|
||||||
msg: Email cannot be empty.
|
msg: Email cannot be empty.
|
||||||
|
pass:
|
||||||
|
label: Current Password
|
||||||
|
msg: Password cannot be empty.
|
||||||
password_title: Password
|
password_title: Password
|
||||||
current_pass:
|
current_pass:
|
||||||
label: Current Password
|
label: Current Password
|
||||||
|
@ -786,6 +800,13 @@ ui:
|
||||||
lang:
|
lang:
|
||||||
label: Interface Language
|
label: Interface Language
|
||||||
text: User interface language. It will change when you refresh the page.
|
text: User interface language. It will change when you refresh the page.
|
||||||
|
my_logins:
|
||||||
|
title: My Logins
|
||||||
|
label: Log in or sign up on this site using these accounts.
|
||||||
|
modal_title: Remove Login
|
||||||
|
modal_content: Are you sure you want to remove this login from your account?
|
||||||
|
modal_confirm_btn: Remove
|
||||||
|
remove_success: Removed successfully
|
||||||
toast:
|
toast:
|
||||||
update: update success
|
update: update success
|
||||||
update_password: Password changed successfully.
|
update_password: Password changed successfully.
|
||||||
|
@ -907,7 +928,6 @@ ui:
|
||||||
modal_confirm:
|
modal_confirm:
|
||||||
title: Error...
|
title: Error...
|
||||||
account_result:
|
account_result:
|
||||||
page_title: Welcome to {{site_name}}
|
|
||||||
success: Your new account is confirmed; you will be redirected to the home page.
|
success: Your new account is confirmed; you will be redirected to the home page.
|
||||||
link: Continue to homepage
|
link: Continue to homepage
|
||||||
invalid: >-
|
invalid: >-
|
||||||
|
@ -1034,6 +1054,7 @@ ui:
|
||||||
admin_name:
|
admin_name:
|
||||||
label: Name
|
label: Name
|
||||||
msg: Name cannot be empty.
|
msg: Name cannot be empty.
|
||||||
|
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||||
admin_password:
|
admin_password:
|
||||||
label: Password
|
label: Password
|
||||||
text: >-
|
text: >-
|
||||||
|
@ -1097,6 +1118,14 @@ ui:
|
||||||
themes: Themes
|
themes: Themes
|
||||||
css-html: CSS/HTML
|
css-html: CSS/HTML
|
||||||
login: Login
|
login: Login
|
||||||
|
plugins: Plugins
|
||||||
|
installed_plugins: Installed Plugins
|
||||||
|
website_welcome: Welcome to {{site_name}}
|
||||||
|
plugins:
|
||||||
|
oauth:
|
||||||
|
connect: Connect with {{ auth_name }}
|
||||||
|
remove: Remove {{ auth_name }}
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
admin_header:
|
admin_header:
|
||||||
title: Admin
|
title: Admin
|
||||||
|
@ -1411,6 +1440,24 @@ ui:
|
||||||
title: Private
|
title: Private
|
||||||
label: Login required
|
label: Login required
|
||||||
text: Only logged in users can access this community.
|
text: Only logged in users can access this community.
|
||||||
|
installed_plugins:
|
||||||
|
title: Installed Plugins
|
||||||
|
filter:
|
||||||
|
all: All
|
||||||
|
active: Active
|
||||||
|
inactive: Inactive
|
||||||
|
outdated: Outdated
|
||||||
|
plugins:
|
||||||
|
label: Plugins
|
||||||
|
text: Select an existing plugin.
|
||||||
|
name: Name
|
||||||
|
version: Version
|
||||||
|
status: Status
|
||||||
|
action: Action
|
||||||
|
deactivate: Deactivate
|
||||||
|
activate: Activate
|
||||||
|
settings: Settings
|
||||||
|
|
||||||
|
|
||||||
form:
|
form:
|
||||||
optional: (optional)
|
optional: (optional)
|
||||||
|
@ -1418,6 +1465,8 @@ ui:
|
||||||
invalid: is invalid
|
invalid: is invalid
|
||||||
btn_submit: Save
|
btn_submit: Save
|
||||||
not_found_props: "Required property {{ key }} not found."
|
not_found_props: "Required property {{ key }} not found."
|
||||||
|
select: Select
|
||||||
|
|
||||||
page_review:
|
page_review:
|
||||||
review: Review
|
review: Review
|
||||||
proposed: proposed
|
proposed: proposed
|
||||||
|
|
|
@ -1053,6 +1053,7 @@ ui:
|
||||||
themes: 主题
|
themes: 主题
|
||||||
css-html: CSS/HTML
|
css-html: CSS/HTML
|
||||||
login: 登录
|
login: 登录
|
||||||
|
website_welcome: 欢迎来到 {{site_name}}
|
||||||
admin:
|
admin:
|
||||||
admin_header:
|
admin_header:
|
||||||
title: 后台管理
|
title: 后台管理
|
||||||
|
|
|
@ -92,10 +92,16 @@ export const ADMIN_NAV_MENUS = [
|
||||||
{ name: 'login' },
|
{ name: 'login' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'plugins',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'installed_plugins',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ADMIN_LEGAL_MENUS = [{ name: 'tos' }, { name: 'privacy' }];
|
|
||||||
|
|
||||||
export const TIMEZONES = [
|
export const TIMEZONES = [
|
||||||
{
|
{
|
||||||
label: 'Africa',
|
label: 'Africa',
|
||||||
|
@ -585,7 +591,7 @@ export const TIMEZONES = [
|
||||||
options: [{ value: 'UTC', label: 'UTC' }],
|
options: [{ value: 'UTC', label: 'UTC' }],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
export const DEFAULT_TIMEZONE = 'UTC+0';
|
export const DEFAULT_TIMEZONE = 'UTC';
|
||||||
|
|
||||||
export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||||
'undeleted',
|
'undeleted',
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||||
|
|
||||||
export interface FormValue<T = any> {
|
export interface FormValue<T = any> {
|
||||||
value: T;
|
value: T;
|
||||||
isInvalid: boolean;
|
isInvalid: boolean;
|
||||||
|
@ -136,6 +138,7 @@ export interface UserInfoRes extends UserInfoBase {
|
||||||
mail_status: number;
|
mail_status: number;
|
||||||
language: string;
|
language: string;
|
||||||
e_mail?: string;
|
e_mail?: string;
|
||||||
|
have_password: boolean;
|
||||||
[prop: string]: any;
|
[prop: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +272,11 @@ export type UserFilterBy =
|
||||||
| 'suspended'
|
| 'suspended'
|
||||||
| 'deleted';
|
| 'deleted';
|
||||||
|
|
||||||
|
export type InstalledPluginsFilterBy =
|
||||||
|
| 'all'
|
||||||
|
| 'active'
|
||||||
|
| 'inactive'
|
||||||
|
| 'outdated';
|
||||||
/**
|
/**
|
||||||
* @description interface for Flags
|
* @description interface for Flags
|
||||||
*/
|
*/
|
||||||
|
@ -527,6 +535,44 @@ export interface User {
|
||||||
avatar: string;
|
avatar: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OauthBindEmailReq {
|
||||||
|
binding_key: string;
|
||||||
|
email: string;
|
||||||
|
must: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OauthConnectorItem {
|
||||||
|
icon: string;
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserOauthConnectorItem extends OauthConnectorItem {
|
||||||
|
binding: boolean;
|
||||||
|
external_id: string;
|
||||||
|
}
|
||||||
|
export interface PluginOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginItem {
|
||||||
|
name: string;
|
||||||
|
type: UIWidget;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
ui_options?: UIOptions;
|
||||||
|
options?: PluginOption[];
|
||||||
|
value?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginConfig {
|
||||||
|
name: string;
|
||||||
|
slug_name: string;
|
||||||
|
config_fields: PluginItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface QuestionOperationReq {
|
export interface QuestionOperationReq {
|
||||||
id: string;
|
id: string;
|
||||||
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
||||||
|
|
|
@ -21,7 +21,7 @@ function MenuNode({
|
||||||
const href = isLeaf ? `${path}${menu.name}` : '#';
|
const href = isLeaf ? `${path}${menu.name}` : '#';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nav.Item key={menu.name}>
|
<Nav.Item key={menu.name} className="w-100">
|
||||||
<Nav.Link
|
<Nav.Link
|
||||||
eventKey={menu.name}
|
eventKey={menu.name}
|
||||||
as={isLeaf ? 'a' : 'button'}
|
as={isLeaf ? 'a' : 'button'}
|
||||||
|
@ -33,7 +33,9 @@ function MenuNode({
|
||||||
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
||||||
{ expanding, 'link-dark': activeKey !== menu.name },
|
{ expanding, 'link-dark': activeKey !== menu.name },
|
||||||
)}>
|
)}>
|
||||||
<span className="me-auto">{t(menu.name)}</span>
|
<span className="me-auto text-truncate">
|
||||||
|
{menu.displayName ? menu.displayName : t(menu.name)}
|
||||||
|
</span>
|
||||||
{menu.badgeContent ? (
|
{menu.badgeContent ? (
|
||||||
<span className="badge text-bg-dark">{menu.badgeContent}</span>
|
<span className="badge text-bg-dark">{menu.badgeContent}</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -114,7 +116,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOpenKey(getOpenKey());
|
setOpenKey(getOpenKey());
|
||||||
}, [activeKey]);
|
}, [activeKey, menus]);
|
||||||
return (
|
return (
|
||||||
<Accordion activeKey={openKey} flush>
|
<Accordion activeKey={openKey} flush>
|
||||||
<Nav variant="pills" className="flex-column" activeKey={activeKey}>
|
<Nav variant="pills" className="flex-column" activeKey={activeKey}>
|
||||||
|
|
|
@ -18,12 +18,16 @@ const Index = ({
|
||||||
title = '',
|
title = '',
|
||||||
confirmText = '',
|
confirmText = '',
|
||||||
content,
|
content,
|
||||||
|
onCancel: onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
cancelBtnVariant = 'link',
|
cancelBtnVariant = 'link',
|
||||||
confirmBtnVariant = 'primary',
|
confirmBtnVariant = 'primary',
|
||||||
...props
|
...props
|
||||||
}: Config) => {
|
}: Config) => {
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
if (typeof onClose === 'function') {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
render({ visible: false });
|
render({ visible: false });
|
||||||
div.remove();
|
div.remove();
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
||||||
|
import { WelcomeTitle } from '@/components';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -32,7 +33,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body className="p-5">
|
<Modal.Body className="p-5">
|
||||||
<div className="d-flex flex-column align-items-center text-center text-body">
|
<div className="d-flex flex-column align-items-center text-center text-body">
|
||||||
<h3>{t('page_title', { site_name: siteInfo.name })}</h3>
|
<WelcomeTitle className="mb-2" />
|
||||||
<p>{siteInfo.description}</p>
|
<p>{siteInfo.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-grid gap-2">
|
<div className="d-grid gap-2">
|
||||||
|
|
|
@ -51,7 +51,7 @@ const Index: React.FC<IProps> = ({
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder={t('placeholder')}
|
placeholder={t('placeholder')}
|
||||||
isInvalid={captcha.isInvalid}
|
isInvalid={captcha?.isInvalid}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||||
handleCaptcha({
|
handleCaptcha({
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
# SchemaForm User Guide
|
---
|
||||||
|
sidebar_position: 0
|
||||||
|
---
|
||||||
|
# Schema Form
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
SchemaForm is a component that can be used to render a form based on a [JSON schema](https://json-schema.org/understanding-json-schema/index.html).
|
A React component capable of building HTML forms out of a [JSON schema](https://json-schema.org/understanding-json-schema/index.html).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
||||||
|
|
||||||
const schema: JSONSchema = {
|
const schema: JSONSchema = {
|
||||||
type: 'object',
|
title: 'General',
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -23,12 +25,12 @@ const schema: JSONSchema = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
title: 'Age',
|
title: 'Age',
|
||||||
},
|
},
|
||||||
sex:{
|
sex: {
|
||||||
type: 'boolean',
|
type: 'string',
|
||||||
title: 'sex',
|
title: 'sex',
|
||||||
enum: [1, 2],
|
enum: [1, 2],
|
||||||
enumNames: ['male', 'female'],
|
enumNames: ['male', 'female'],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -39,41 +41,193 @@ const uiSchema: UISchema = {
|
||||||
age: {
|
age: {
|
||||||
'ui:widget': 'input',
|
'ui:widget': 'input',
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'number'
|
type: 'number',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
sex: {
|
sex: {
|
||||||
'ui:widget': 'radio',
|
'ui:widget': 'radio',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// form component
|
|
||||||
|
|
||||||
const Form = () => {
|
const Form = () => {
|
||||||
const [formData, setFormData] = useState(initFormData(schema));
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
||||||
|
const handleChange = (data) => {
|
||||||
|
setFormData(data);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaForm
|
<SchemaForm
|
||||||
schema={schema}
|
schema={schema}
|
||||||
uiSchema={uiSchema}
|
uiSchema={uiSchema}
|
||||||
formData={formData}
|
formData={formData}
|
||||||
onChange={console.log}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Form;
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
| Property | Description | Type | Default |
|
| Property | Description | Type | Default |
|
||||||
| -------- | ---------------------------------------- | ----------------------------------------- | ------- |
|
| -------- | ---------------------------------------- | ------------------------------------- | ------- |
|
||||||
| schema | JSON schema | [JSONSchema](index.tsx#L9) | - |
|
| schema | Describe the form structure with schema | [JSONSchema](#json-schema) | - |
|
||||||
| uiSchema | UI schema | [UISchema](index.tsx#L24) | - |
|
| uiSchema | Describe the properties of the field | [UISchema](#uischema) | - |
|
||||||
| formData | Form data | [FormData](index.tsx#L66) | - |
|
| formData | Describe form data | [FormData](#formdata) | - |
|
||||||
| onChange | Callback function when form data changes | (data: [FormData](index.tsx#L66)) => void | - |
|
| onChange | Callback function when form data changes | (data: [FormData](#formdata)) => void | - |
|
||||||
| onSubmit | Callback function when form is submitted | (data: React.FormEvent) => void | - |
|
| onSubmit | Callback function when form is submitted | (data: React.FormEvent) => void | - |
|
||||||
|
|
||||||
|
## Types Definition
|
||||||
|
### JSONSchema
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface JSONSchema {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
required?: string[];
|
||||||
|
properties: {
|
||||||
|
[key: string]: {
|
||||||
|
type: 'string' | 'boolean' | 'number';
|
||||||
|
title: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
enum?: Array<string | boolean | number>;
|
||||||
|
enumNames?: string[];
|
||||||
|
default?: string | boolean | number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UIOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface UIOptions {
|
||||||
|
empty?: string;
|
||||||
|
className?: string | string[];
|
||||||
|
validator?: (
|
||||||
|
value,
|
||||||
|
formData?,
|
||||||
|
) => Promise<string | true | void> | true | string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### InputOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface InputOptions extends UIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
type?:
|
||||||
|
| 'color'
|
||||||
|
| 'date'
|
||||||
|
| 'datetime-local'
|
||||||
|
| 'email'
|
||||||
|
| 'month'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'range'
|
||||||
|
| 'search'
|
||||||
|
| 'tel'
|
||||||
|
| 'text'
|
||||||
|
| 'time'
|
||||||
|
| 'url'
|
||||||
|
| 'week';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### SelectOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface SelectOptions extends UIOptions {}
|
||||||
|
```
|
||||||
|
### UploadOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface UploadOptions extends UIOptions {
|
||||||
|
acceptType?: string;
|
||||||
|
imageType?: 'post' | 'avatar' | 'branding';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### SwitchOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface SwitchOptions extends UIOptions {}
|
||||||
|
```
|
||||||
|
### TimezoneOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface TimezoneOptions extends UIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### CheckboxOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface CheckboxOptions extends UIOptions {}
|
||||||
|
```
|
||||||
|
### RadioOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface RadioOptions extends UIOptions {}
|
||||||
|
```
|
||||||
|
### TextareaOptions
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface TextareaOptions extends UIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### UIWidget
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type UIWidget =
|
||||||
|
| 'textarea'
|
||||||
|
| 'input'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'radio'
|
||||||
|
| 'select'
|
||||||
|
| 'upload'
|
||||||
|
| 'timezone'
|
||||||
|
| 'switch';
|
||||||
|
```
|
||||||
|
|
||||||
|
### UISchema
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface UISchema {
|
||||||
|
[key: string]: {
|
||||||
|
'ui:widget'?: UIWidget;
|
||||||
|
'ui:options'?:
|
||||||
|
| InputOptions
|
||||||
|
| SelectOptions
|
||||||
|
| UploadOptions
|
||||||
|
| SwitchOptions
|
||||||
|
| TimezoneOptions
|
||||||
|
| CheckboxOptions
|
||||||
|
| RadioOptions
|
||||||
|
| TextareaOptions;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### FormData
|
||||||
|
```ts
|
||||||
|
export interface FormValue<T = any> {
|
||||||
|
value: T;
|
||||||
|
isInvalid: boolean;
|
||||||
|
errorMsg: string;
|
||||||
|
[prop: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormDataType {
|
||||||
|
[prop: string]: FormValue;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## reference
|
## reference
|
||||||
|
|
||||||
- [json schema](https://json-schema.org/understanding-json-schema/index.html)
|
- [json schema](https://json-schema.org/understanding-json-schema/index.html)
|
||||||
- [react-jsonschema-form](http://rjsf-team.github.io/react-jsonschema-form/)
|
- [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form)
|
||||||
- [vue-json-schema-form](https://github.com/lljj-x/vue-json-schema-form/)
|
- [vue-json-schema-form](https://github.com/lljj-x/vue-json-schema-form/)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
ForwardRefRenderFunction,
|
ForwardRefRenderFunction,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Form, Button, Stack } from 'react-bootstrap';
|
import { Form, Button, Stack } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -20,7 +21,6 @@ export interface JSONSchema {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
type: 'string' | 'boolean' | 'number';
|
type: 'string' | 'boolean' | 'number';
|
||||||
title: string;
|
title: string;
|
||||||
label?: string;
|
|
||||||
description?: string;
|
description?: string;
|
||||||
enum?: Array<string | boolean | number>;
|
enum?: Array<string | boolean | number>;
|
||||||
enumNames?: string[];
|
enumNames?: string[];
|
||||||
|
@ -28,45 +28,79 @@ export interface JSONSchema {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseUIOptions {
|
||||||
|
empty?: string;
|
||||||
|
className?: string | string[];
|
||||||
|
validator?: (
|
||||||
|
value,
|
||||||
|
formData?,
|
||||||
|
) => Promise<string | true | void> | true | string;
|
||||||
|
}
|
||||||
|
export interface InputOptions extends BaseUIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
inputType?:
|
||||||
|
| 'color'
|
||||||
|
| 'date'
|
||||||
|
| 'datetime-local'
|
||||||
|
| 'email'
|
||||||
|
| 'month'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'range'
|
||||||
|
| 'search'
|
||||||
|
| 'tel'
|
||||||
|
| 'text'
|
||||||
|
| 'time'
|
||||||
|
| 'url'
|
||||||
|
| 'week';
|
||||||
|
}
|
||||||
|
export interface SelectOptions extends BaseUIOptions {}
|
||||||
|
export interface UploadOptions extends BaseUIOptions {
|
||||||
|
acceptType?: string;
|
||||||
|
imageType?: Type.UploadType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SwitchOptions extends BaseUIOptions {
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimezoneOptions extends BaseUIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CheckboxOptions extends BaseUIOptions {}
|
||||||
|
|
||||||
|
export interface RadioOptions extends BaseUIOptions {}
|
||||||
|
|
||||||
|
export interface TextareaOptions extends BaseUIOptions {
|
||||||
|
placeholder?: string;
|
||||||
|
rows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UIOptions =
|
||||||
|
| InputOptions
|
||||||
|
| SelectOptions
|
||||||
|
| UploadOptions
|
||||||
|
| SwitchOptions
|
||||||
|
| TimezoneOptions
|
||||||
|
| CheckboxOptions
|
||||||
|
| RadioOptions
|
||||||
|
| TextareaOptions;
|
||||||
|
|
||||||
|
export type UIWidget =
|
||||||
|
| 'textarea'
|
||||||
|
| 'input'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'radio'
|
||||||
|
| 'select'
|
||||||
|
| 'upload'
|
||||||
|
| 'timezone'
|
||||||
|
| 'switch';
|
||||||
export interface UISchema {
|
export interface UISchema {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
'ui:widget'?:
|
'ui:widget'?: UIWidget;
|
||||||
| 'textarea'
|
'ui:options'?: UIOptions;
|
||||||
| 'text'
|
|
||||||
| 'checkbox'
|
|
||||||
| 'radio'
|
|
||||||
| 'select'
|
|
||||||
| 'upload'
|
|
||||||
| 'timezone'
|
|
||||||
| 'switch';
|
|
||||||
'ui:options'?: {
|
|
||||||
rows?: number;
|
|
||||||
placeholder?: string;
|
|
||||||
type?:
|
|
||||||
| 'color'
|
|
||||||
| 'date'
|
|
||||||
| 'datetime-local'
|
|
||||||
| 'email'
|
|
||||||
| 'month'
|
|
||||||
| 'number'
|
|
||||||
| 'password'
|
|
||||||
| 'range'
|
|
||||||
| 'search'
|
|
||||||
| 'tel'
|
|
||||||
| 'text'
|
|
||||||
| 'time'
|
|
||||||
| 'url'
|
|
||||||
| 'week';
|
|
||||||
empty?: string;
|
|
||||||
className?: string | string[];
|
|
||||||
validator?: (
|
|
||||||
value,
|
|
||||||
formData?,
|
|
||||||
) => Promise<string | true | void> | true | string;
|
|
||||||
textRender?: () => React.ReactElement;
|
|
||||||
imageType?: Type.UploadType;
|
|
||||||
acceptType?: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +150,30 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(properties);
|
const keys = Object.keys(properties);
|
||||||
|
/**
|
||||||
|
* Prevent components such as `select` from having default values,
|
||||||
|
* which are not generated on `formData`
|
||||||
|
*/
|
||||||
|
const setDefaultValueAsDomBehaviour = () => {
|
||||||
|
keys.forEach((k) => {
|
||||||
|
const fieldVal = formData[k]?.value;
|
||||||
|
const metaProp = properties[k];
|
||||||
|
const uiCtrl = uiSchema[k]?.['ui:widget'];
|
||||||
|
if (!metaProp || !uiCtrl || fieldVal !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (uiCtrl === 'select' && metaProp.enum?.[0] !== undefined) {
|
||||||
|
formData[k] = {
|
||||||
|
errorMsg: '',
|
||||||
|
isInvalid: false,
|
||||||
|
value: metaProp.enum?.[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
setDefaultValueAsDomBehaviour();
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
|
@ -265,16 +323,17 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckboxChange = (
|
const handleInputCheck = (
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
index: number,
|
index: number,
|
||||||
) => {
|
) => {
|
||||||
const { name } = e.target;
|
const { name, checked } = e.currentTarget;
|
||||||
|
const freshVal = checked ? schema.properties[name]?.enum?.[index] : '';
|
||||||
const data = {
|
const data = {
|
||||||
...formData,
|
...formData,
|
||||||
[name]: {
|
[name]: {
|
||||||
...formData[name],
|
...formData[name],
|
||||||
value: schema.properties[name]?.enum?.[index],
|
value: freshVal,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -286,12 +345,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
validator,
|
validator,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
{keys.map((key) => {
|
{keys.map((key) => {
|
||||||
const { title, description, label } = properties[key];
|
const { title, description } = properties[key];
|
||||||
const { 'ui:widget': widget = 'input', 'ui:options': options = {} } =
|
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||||
uiSchema[key] || {};
|
uiSchema[key] || {};
|
||||||
if (widget === 'select') {
|
if (widget === 'select') {
|
||||||
return (
|
return (
|
||||||
|
@ -303,7 +361,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
<Form.Select
|
<Form.Select
|
||||||
aria-label={description}
|
aria-label={description}
|
||||||
name={key}
|
name={key}
|
||||||
value={formData[key]?.value}
|
value={formData[key]?.value || ''}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
isInvalid={formData[key].isInvalid}>
|
isInvalid={formData[key].isInvalid}>
|
||||||
{properties[key].enum?.map((item, index) => {
|
{properties[key].enum?.map((item, index) => {
|
||||||
|
@ -323,6 +381,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'checkbox' || widget === 'radio') {
|
if (widget === 'checkbox' || widget === 'radio') {
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
|
@ -341,11 +400,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
name={key}
|
name={key}
|
||||||
id={`form-${String(item)}`}
|
id={`form-${String(item)}`}
|
||||||
label={properties[key].enumNames?.[index]}
|
label={properties[key].enumNames?.[index]}
|
||||||
checked={formData[key]?.value === item}
|
checked={(formData[key]?.value || '') === item}
|
||||||
feedback={formData[key]?.errorMsg}
|
feedback={formData[key]?.errorMsg}
|
||||||
feedbackType="invalid"
|
feedbackType="invalid"
|
||||||
isInvalid={formData[key].isInvalid}
|
isInvalid={formData[key].isInvalid}
|
||||||
onChange={(e) => handleCheckboxChange(e, index)}
|
onChange={(e) => handleInputCheck(e, index)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -372,8 +431,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
id={`switch-${title}`}
|
id={`switch-${title}`}
|
||||||
name={key}
|
name={key}
|
||||||
type="switch"
|
type="switch"
|
||||||
label={label}
|
label={(uiOpt as SwitchOptions)?.label}
|
||||||
checked={formData[key]?.value}
|
checked={formData[key]?.value || ''}
|
||||||
feedback={formData[key]?.errorMsg}
|
feedback={formData[key]?.errorMsg}
|
||||||
feedbackType="invalid"
|
feedbackType="invalid"
|
||||||
isInvalid={formData[key].isInvalid}
|
isInvalid={formData[key].isInvalid}
|
||||||
|
@ -396,7 +455,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
controlId={key}>
|
controlId={key}>
|
||||||
<Form.Label>{title}</Form.Label>
|
<Form.Label>{title}</Form.Label>
|
||||||
<TimeZonePicker
|
<TimeZonePicker
|
||||||
value={formData[key]?.value}
|
value={formData[key]?.value || ''}
|
||||||
name={key}
|
name={key}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
/>
|
/>
|
||||||
|
@ -416,6 +475,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'upload') {
|
if (widget === 'upload') {
|
||||||
|
const options: UploadOptions = uiSchema[key]?.['ui:options'] || {};
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
key={title}
|
key={title}
|
||||||
|
@ -444,6 +504,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget === 'textarea') {
|
if (widget === 'textarea') {
|
||||||
|
const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
controlId={`form-${key}`}
|
controlId={`form-${key}`}
|
||||||
|
@ -454,8 +516,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
as="textarea"
|
as="textarea"
|
||||||
name={key}
|
name={key}
|
||||||
placeholder={options?.placeholder || ''}
|
placeholder={options?.placeholder || ''}
|
||||||
type={options?.type || 'text'}
|
value={formData[key]?.value || ''}
|
||||||
value={formData[key]?.value}
|
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
isInvalid={formData[key].isInvalid}
|
isInvalid={formData[key].isInvalid}
|
||||||
rows={options?.rows || 3}
|
rows={options?.rows || 3}
|
||||||
|
@ -471,6 +532,9 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const options: InputOptions = uiSchema[key]?.['ui:options'] || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group
|
<Form.Group
|
||||||
controlId={key}
|
controlId={key}
|
||||||
|
@ -480,10 +544,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
<Form.Control
|
<Form.Control
|
||||||
name={key}
|
name={key}
|
||||||
placeholder={options?.placeholder || ''}
|
placeholder={options?.placeholder || ''}
|
||||||
type={options?.type || 'text'}
|
type={options?.inputType || 'text'}
|
||||||
value={formData[key]?.value}
|
value={formData[key]?.value || ''}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
style={options?.type === 'color' ? { width: '6rem' } : {}}
|
style={options?.inputType === 'color' ? { width: '6rem' } : {}}
|
||||||
isInvalid={formData[key].isInvalid}
|
isInvalid={formData[key].isInvalid}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
|
@ -507,10 +571,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||||
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||||
const formData: Type.FormDataType = {};
|
const formData: Type.FormDataType = {};
|
||||||
Object.keys(schema.properties).forEach((key) => {
|
Object.keys(schema.properties).forEach((key) => {
|
||||||
const v = schema.properties[key]?.default;
|
const prop = schema.properties[key];
|
||||||
// TODO: set default value by property type
|
const defaultVal = prop?.default;
|
||||||
formData[key] = {
|
formData[key] = {
|
||||||
value: typeof v !== 'undefined' ? v : '',
|
value: defaultVal,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React, { FC, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { siteInfoStore } from '@/stores';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
as?: React.ElementType;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Index: FC<Props> = ({ as: Component = 'h3', className = 'mb-5' }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||||
|
return (
|
||||||
|
<Component className={classnames('text-center', className)}>
|
||||||
|
{t('website_welcome', { site_name: siteName })}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -32,6 +32,7 @@ import CustomizeTheme from './CustomizeTheme';
|
||||||
import PageTags from './PageTags';
|
import PageTags from './PageTags';
|
||||||
import QuestionListLoader from './QuestionListLoader';
|
import QuestionListLoader from './QuestionListLoader';
|
||||||
import TagsLoader from './TagsLoader';
|
import TagsLoader from './TagsLoader';
|
||||||
|
import WelcomeTitle from './WelcomeTitle';
|
||||||
import Counts from './Counts';
|
import Counts from './Counts';
|
||||||
import QuestionList from './QuestionList';
|
import QuestionList from './QuestionList';
|
||||||
import HotQuestions from './HotQuestions';
|
import HotQuestions from './HotQuestions';
|
||||||
|
@ -74,6 +75,7 @@ export {
|
||||||
PageTags,
|
PageTags,
|
||||||
QuestionListLoader,
|
QuestionListLoader,
|
||||||
TagsLoader,
|
TagsLoader,
|
||||||
|
WelcomeTitle,
|
||||||
Counts,
|
Counts,
|
||||||
QuestionList,
|
QuestionList,
|
||||||
HotQuestions,
|
HotQuestions,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import useChangeUserRoleModal from './useChangeUserRoleModal';
|
||||||
import useUserModal from './useUserModal';
|
import useUserModal from './useUserModal';
|
||||||
import useChangePasswordModal from './useChangePasswordModal';
|
import useChangePasswordModal from './useChangePasswordModal';
|
||||||
import usePageTags from './usePageTags';
|
import usePageTags from './usePageTags';
|
||||||
|
import useLoginRedirect from './useLoginRedirect';
|
||||||
import usePromptWithUnload from './usePrompt';
|
import usePromptWithUnload from './usePrompt';
|
||||||
import useImgViewer from './useImgViewer';
|
import useImgViewer from './useImgViewer';
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ export {
|
||||||
useUserModal,
|
useUserModal,
|
||||||
useChangePasswordModal,
|
useChangePasswordModal,
|
||||||
usePageTags,
|
usePageTags,
|
||||||
|
useLoginRedirect,
|
||||||
usePromptWithUnload,
|
usePromptWithUnload,
|
||||||
useImgViewer,
|
useImgViewer,
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,7 @@ const useChangePasswordModal = (props: IProps = {}) => {
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
password: {
|
password: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'password',
|
inputType: 'password',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
const MIN_LENGTH = 8;
|
const MIN_LENGTH = 8;
|
||||||
const MAX_LENGTH = 32;
|
const MAX_LENGTH = 32;
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
|
import Storage from '@/utils/storage';
|
||||||
|
import { RouteAlias } from '@/router/alias';
|
||||||
|
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const loginRedirect = () => {
|
||||||
|
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||||
|
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||||
|
floppyNavigation.navigate(redirect, () => {
|
||||||
|
navigate(redirect, { replace: true });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { loginRedirect };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -57,7 +57,7 @@ const useAddUserModal = (props: IProps = {}) => {
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'email',
|
inputType: 'email',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
if (value && !pattern.email.test(value)) {
|
if (value && !pattern.email.test(value)) {
|
||||||
return t('form.fields.email.msg');
|
return t('form.fields.email.msg');
|
||||||
|
@ -68,7 +68,7 @@ const useAddUserModal = (props: IProps = {}) => {
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'password',
|
inputType: 'password',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
const MIN_LENGTH = 8;
|
const MIN_LENGTH = 8;
|
||||||
const MAX_LENGTH = 32;
|
const MAX_LENGTH = 32;
|
||||||
|
|
|
@ -315,3 +315,13 @@ img:not(a img, img.broken) {
|
||||||
.bg-fade-out {
|
.bg-fade-out {
|
||||||
animation: bg-fade-out 2s ease 0.3s;
|
animation: bg-fade-out 2s ease 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btnSvg, .btnSvg:hover {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import React from 'react';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
import { guard } from '@/utils';
|
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
@ -12,13 +10,8 @@ const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
async function bootstrapApp() {
|
root.render(
|
||||||
await guard.setupApp();
|
<React.StrictMode>
|
||||||
root.render(
|
<App />
|
||||||
<React.StrictMode>
|
</React.StrictMode>,
|
||||||
<App />
|
);
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrapApp();
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { FC } from 'react';
|
||||||
import { Row, Col } from 'react-bootstrap';
|
import { Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { siteInfoStore } from '@/stores';
|
||||||
import { useDashBoard } from '@/services';
|
import { useDashBoard } from '@/services';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -13,6 +14,7 @@ import {
|
||||||
|
|
||||||
const Dashboard: FC = () => {
|
const Dashboard: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||||
|
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||||
const { data } = useDashBoard();
|
const { data } = useDashBoard();
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
@ -22,7 +24,7 @@ const Dashboard: FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 className="text-capitalize">{t('title')}</h3>
|
<h3 className="text-capitalize">{t('title')}</h3>
|
||||||
<p className="mt-4">{t('welcome')}</p>
|
<p className="mt-4">{t('welcome', { site_name: siteName })}</p>
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={6}>
|
<Col lg={6}>
|
||||||
<Statistics data={data.info} />
|
<Statistics data={data.info} />
|
||||||
|
|
|
@ -55,7 +55,7 @@ const General: FC = () => {
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
site_url: {
|
site_url: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'url',
|
inputType: 'url',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
let url: URL | undefined;
|
let url: URL | undefined;
|
||||||
try {
|
try {
|
||||||
|
@ -79,7 +79,7 @@ const General: FC = () => {
|
||||||
},
|
},
|
||||||
contact_email: {
|
contact_email: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'email',
|
inputType: 'email',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
if (!Pattern.email.test(value)) {
|
if (!Pattern.email.test(value)) {
|
||||||
return t('contact_email.validate');
|
return t('contact_email.validate');
|
||||||
|
|
|
@ -40,11 +40,13 @@ const Interface: FC = () => {
|
||||||
description: t('language.text'),
|
description: t('language.text'),
|
||||||
enum: langs?.map((lang) => lang.value),
|
enum: langs?.map((lang) => lang.value),
|
||||||
enumNames: langs?.map((lang) => lang.label),
|
enumNames: langs?.map((lang) => lang.label),
|
||||||
|
default: setting?.language || storeInterface.language,
|
||||||
},
|
},
|
||||||
time_zone: {
|
time_zone: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: t('time_zone.label'),
|
title: t('time_zone.label'),
|
||||||
description: t('time_zone.text'),
|
description: t('time_zone.text'),
|
||||||
|
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||||
},
|
},
|
||||||
default_avatar: {
|
default_avatar: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|
|
@ -19,14 +19,12 @@ const Index: FC = () => {
|
||||||
allow_new_registrations: {
|
allow_new_registrations: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('membership.title'),
|
title: t('membership.title'),
|
||||||
label: t('membership.label'),
|
|
||||||
description: t('membership.text'),
|
description: t('membership.text'),
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
login_required: {
|
login_required: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('private.title'),
|
title: t('private.title'),
|
||||||
label: t('private.label'),
|
|
||||||
description: t('private.text'),
|
description: t('private.text'),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -35,9 +33,15 @@ const Index: FC = () => {
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
allow_new_registrations: {
|
allow_new_registrations: {
|
||||||
'ui:widget': 'switch',
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('membership.label'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
login_required: {
|
login_required: {
|
||||||
'ui:widget': 'switch',
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('private.label'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [formData, setFormData] = useState(initFormData(schema));
|
const [formData, setFormData] = useState(initFormData(schema));
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import type * as Types from '@/common/interface';
|
||||||
|
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||||
|
import { useQueryPluginConfig, updatePluginConfig } from '@/services';
|
||||||
|
import { InputOptions } from '@/components/SchemaForm';
|
||||||
|
|
||||||
|
const Config = () => {
|
||||||
|
const { t } = useTranslation('translation');
|
||||||
|
const { slug_name } = useParams<{ slug_name: string }>();
|
||||||
|
const { data } = useQueryPluginConfig({ plugin_slug_name: slug_name });
|
||||||
|
const Toast = useToast();
|
||||||
|
const [schema, setSchema] = useState<JSONSchema | null>(null);
|
||||||
|
const [uiSchema, setUISchema] = useState<UISchema>();
|
||||||
|
const required: string[] = [];
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<Types.FormDataType | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const properties: JSONSchema['properties'] = {};
|
||||||
|
const uiConf: UISchema = {};
|
||||||
|
data.config_fields?.forEach((item) => {
|
||||||
|
properties[item.name] = {
|
||||||
|
type: 'string',
|
||||||
|
title: item.title,
|
||||||
|
description: item.description,
|
||||||
|
default: item.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.options instanceof Array) {
|
||||||
|
properties[item.name].enum = item.options.map((option) => option.value);
|
||||||
|
properties[item.name].enumNames = item.options.map(
|
||||||
|
(option) => option.label,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
uiConf[item.name] = {};
|
||||||
|
uiConf[item.name]['ui:widget'] = item.type;
|
||||||
|
if (item.ui_options) {
|
||||||
|
if ((item.ui_options as InputOptions & { input_type })?.input_type) {
|
||||||
|
(item.ui_options as InputOptions).inputType = (
|
||||||
|
item.ui_options as InputOptions & { input_type }
|
||||||
|
).input_type;
|
||||||
|
}
|
||||||
|
uiConf[item.name]['ui:options'] = item.ui_options;
|
||||||
|
}
|
||||||
|
if (item.required) {
|
||||||
|
required.push(item.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = {
|
||||||
|
title: data?.name || '',
|
||||||
|
required,
|
||||||
|
properties,
|
||||||
|
};
|
||||||
|
setSchema(result);
|
||||||
|
setUISchema(uiConf);
|
||||||
|
setFormData(initFormData(result));
|
||||||
|
}, [data?.config_fields]);
|
||||||
|
|
||||||
|
const onSubmit = (evt) => {
|
||||||
|
if (!formData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const config_fields = {};
|
||||||
|
Object.keys(formData).forEach((key) => {
|
||||||
|
config_fields[key] = formData[key].value;
|
||||||
|
});
|
||||||
|
const params = {
|
||||||
|
plugin_slug_name: slug_name,
|
||||||
|
config_fields,
|
||||||
|
};
|
||||||
|
updatePluginConfig(params).then(() => {
|
||||||
|
Toast.onShow({
|
||||||
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChange = (form) => {
|
||||||
|
setFormData(form);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data || !schema || !formData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(schema.properties)) {
|
||||||
|
return <h3 className="mb-4">{data?.name}</h3>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{data?.name}</h3>
|
||||||
|
<SchemaForm
|
||||||
|
schema={schema}
|
||||||
|
uiSchema={uiSchema}
|
||||||
|
formData={formData}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Table, Dropdown, Stack } from 'react-bootstrap';
|
||||||
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { Empty, QueryGroup, Icon } from '@/components';
|
||||||
|
import * as Type from '@/common/interface';
|
||||||
|
import { useQueryPlugins, updatePluginStatus } from '@/services';
|
||||||
|
|
||||||
|
const InstalledPluginsFilterKeys: Type.InstalledPluginsFilterBy[] = [
|
||||||
|
'all',
|
||||||
|
'active',
|
||||||
|
'inactive',
|
||||||
|
];
|
||||||
|
|
||||||
|
const bgMap = {
|
||||||
|
active: 'text-bg-success',
|
||||||
|
inactive: 'text-bg-secondary',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Users: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'admin.installed_plugins',
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [urlSearchParams] = useSearchParams();
|
||||||
|
const curFilter =
|
||||||
|
urlSearchParams.get('filter') || InstalledPluginsFilterKeys[0];
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
mutate: updatePlugins,
|
||||||
|
} = useQueryPlugins({
|
||||||
|
status: curFilter === 'all' ? undefined : curFilter,
|
||||||
|
});
|
||||||
|
const emitPluginChange = (type) => {
|
||||||
|
window.postMessage({
|
||||||
|
msgType: type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleStatus = (plugin) => {
|
||||||
|
updatePluginStatus({
|
||||||
|
enabled: !plugin.enabled,
|
||||||
|
plugin_slug_name: plugin.slug_name,
|
||||||
|
}).then(() => {
|
||||||
|
updatePlugins();
|
||||||
|
if (plugin.have_config) {
|
||||||
|
emitPluginChange('refreshConfigurablePlugins');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSettings = (plugin) => {
|
||||||
|
const url = `/admin/${plugin.slug_name}`;
|
||||||
|
navigate(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<Stack direction="horizontal" gap={3}>
|
||||||
|
<QueryGroup
|
||||||
|
data={InstalledPluginsFilterKeys}
|
||||||
|
currentSort={curFilter}
|
||||||
|
sortKey="filter"
|
||||||
|
i18nKeyPrefix="admin.installed_plugins.filter"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t('name')}</th>
|
||||||
|
<th style={{ width: '17%' }}>{t('version')}</th>
|
||||||
|
<th style={{ width: '11%' }}>{t('status')}</th>
|
||||||
|
{curFilter !== 'deleted' ? (
|
||||||
|
<th style={{ width: '11%' }} className="text-end">
|
||||||
|
{t('action')}
|
||||||
|
</th>
|
||||||
|
) : null}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="align-middle">
|
||||||
|
{data?.map((plugin) => {
|
||||||
|
return (
|
||||||
|
<tr key={plugin.slug_name}>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
{plugin.link ? (
|
||||||
|
<a href={plugin.link} target="_blank" rel="noreferrer">
|
||||||
|
{plugin.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
plugin.name
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="fs-14">{plugin.description}</div>
|
||||||
|
</td>
|
||||||
|
<td className="text-break">{plugin.version}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'badge',
|
||||||
|
bgMap[plugin.enabled ? 'active' : 'inactive'],
|
||||||
|
)}>
|
||||||
|
{t(`filter.${plugin.enabled ? 'active' : 'inactive'}`)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{curFilter !== 'deleted' ? (
|
||||||
|
<td className="text-end">
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||||
|
<Icon name="three-dots-vertical" />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{plugin.enabled ? (
|
||||||
|
<Dropdown.Item onClick={() => handleStatus(plugin)}>
|
||||||
|
{t('deactivate')}
|
||||||
|
</Dropdown.Item>
|
||||||
|
) : (
|
||||||
|
<Dropdown.Item onClick={() => handleStatus(plugin)}>
|
||||||
|
{t('activate')}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
{plugin.enabled && plugin.have_config && (
|
||||||
|
<Dropdown.Item onClick={() => handleSettings(plugin)}>
|
||||||
|
{t('settings')}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
{Number(data?.length) <= 0 && !isLoading && <Empty />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Users;
|
|
@ -33,7 +33,7 @@ const Smtp: FC = () => {
|
||||||
description: t('smtp_host.text'),
|
description: t('smtp_host.text'),
|
||||||
},
|
},
|
||||||
encryption: {
|
encryption: {
|
||||||
type: 'boolean',
|
type: 'string',
|
||||||
title: t('encryption.label'),
|
title: t('encryption.label'),
|
||||||
description: t('encryption.text'),
|
description: t('encryption.text'),
|
||||||
enum: ['SSL', ''],
|
enum: ['SSL', ''],
|
||||||
|
@ -47,7 +47,6 @@ const Smtp: FC = () => {
|
||||||
smtp_authentication: {
|
smtp_authentication: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('smtp_authentication.title'),
|
title: t('smtp_authentication.title'),
|
||||||
label: t('smtp_authentication.label'),
|
|
||||||
enum: [true, false],
|
enum: [true, false],
|
||||||
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
||||||
},
|
},
|
||||||
|
@ -69,7 +68,7 @@ const Smtp: FC = () => {
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
from_email: {
|
from_email: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'email',
|
inputType: 'email',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
encryption: {
|
encryption: {
|
||||||
|
@ -89,7 +88,7 @@ const Smtp: FC = () => {
|
||||||
},
|
},
|
||||||
smtp_password: {
|
smtp_password: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'password',
|
inputType: 'password',
|
||||||
validator: (value: string, formData) => {
|
validator: (value: string, formData) => {
|
||||||
if (formData.smtp_authentication.value) {
|
if (formData.smtp_authentication.value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
@ -102,10 +101,13 @@ const Smtp: FC = () => {
|
||||||
},
|
},
|
||||||
smtp_authentication: {
|
smtp_authentication: {
|
||||||
'ui:widget': 'switch',
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('smtp_authentication.label'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
smtp_port: {
|
smtp_port: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'number',
|
inputType: 'number',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) {
|
if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) {
|
||||||
return t('smtp_port.msg');
|
return t('smtp_port.msg');
|
||||||
|
@ -116,7 +118,7 @@ const Smtp: FC = () => {
|
||||||
},
|
},
|
||||||
test_email_recipient: {
|
test_email_recipient: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'email',
|
inputType: 'email',
|
||||||
validator: (value) => {
|
validator: (value) => {
|
||||||
if (value && !pattern.email.test(value)) {
|
if (value && !pattern.email.test(value)) {
|
||||||
return t('test_email_recipient.msg');
|
return t('test_email_recipient.msg');
|
||||||
|
@ -177,7 +179,7 @@ const Smtp: FC = () => {
|
||||||
}, [setting]);
|
}, [setting]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.smtp_authentication.value === '') {
|
if (!/true|false/.test(formData.smtp_authentication.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (formData.smtp_authentication.value) {
|
if (formData.smtp_authentication.value) {
|
||||||
|
|
|
@ -50,7 +50,7 @@ const Index: FC = () => {
|
||||||
},
|
},
|
||||||
primary_color: {
|
primary_color: {
|
||||||
'ui:options': {
|
'ui:options': {
|
||||||
type: 'color',
|
inputType: 'color',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,7 +27,6 @@ const Index: FC = () => {
|
||||||
required_tag: {
|
required_tag: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('required_tag.title'),
|
title: t('required_tag.title'),
|
||||||
label: t('required_tag.label'),
|
|
||||||
description: t('required_tag.text'),
|
description: t('required_tag.text'),
|
||||||
},
|
},
|
||||||
reserved_tags: {
|
reserved_tags: {
|
||||||
|
@ -46,6 +45,9 @@ const Index: FC = () => {
|
||||||
},
|
},
|
||||||
required_tag: {
|
required_tag: {
|
||||||
'ui:widget': 'switch',
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('required_tag.label'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
reserved_tags: {
|
reserved_tags: {
|
||||||
'ui:widget': 'textarea',
|
'ui:widget': 'textarea',
|
||||||
|
|
|
@ -1,29 +1,68 @@
|
||||||
import { FC } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import { Container, Row, Col } from 'react-bootstrap';
|
import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Outlet, useLocation } from 'react-router-dom';
|
import { Outlet, useMatch } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
import { AccordionNav } from '@/components';
|
import { AccordionNav } from '@/components';
|
||||||
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||||
|
import { useQueryPlugins } from '@/services';
|
||||||
|
import { interfaceStore } from '@/stores';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const formPaths = [
|
const g10Paths = [
|
||||||
'general',
|
'dashboard',
|
||||||
'smtp',
|
'questions',
|
||||||
'interface',
|
'answers',
|
||||||
'branding',
|
'users',
|
||||||
'legal',
|
'flags',
|
||||||
'write',
|
'installed_plugins',
|
||||||
'seo',
|
|
||||||
'themes',
|
|
||||||
'css-html',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
const { pathname } = useLocation();
|
const pathMatch = useMatch('/admin/:path');
|
||||||
|
const curPath = pathMatch?.params.path || 'dashboard';
|
||||||
|
|
||||||
|
const interfaceLang = interfaceStore((_) => _.interface.language);
|
||||||
|
const { data: configurablePlugins, mutate: updateConfigurablePlugins } =
|
||||||
|
useQueryPlugins({
|
||||||
|
status: 'active',
|
||||||
|
have_config: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const menus = cloneDeep(ADMIN_NAV_MENUS);
|
||||||
|
if (configurablePlugins && configurablePlugins.length > 0) {
|
||||||
|
menus.forEach((item) => {
|
||||||
|
if (item.name === 'plugins' && item.children) {
|
||||||
|
item.children = [
|
||||||
|
...item.children,
|
||||||
|
...configurablePlugins.map((plugin) => ({
|
||||||
|
name: plugin.slug_name,
|
||||||
|
displayName: plugin.name,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const observePlugins = (evt) => {
|
||||||
|
if (evt.data.msgType === 'refreshConfigurablePlugins') {
|
||||||
|
updateConfigurablePlugins();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('message', observePlugins);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', observePlugins);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
updateConfigurablePlugins();
|
||||||
|
}, [interfaceLang]);
|
||||||
|
|
||||||
usePageTags({
|
usePageTags({
|
||||||
title: t('admin'),
|
title: t('admin'),
|
||||||
});
|
});
|
||||||
|
@ -39,9 +78,9 @@ const Index: FC = () => {
|
||||||
<Container className="admin-container">
|
<Container className="admin-container">
|
||||||
<Row>
|
<Row>
|
||||||
<Col lg={2}>
|
<Col lg={2}>
|
||||||
<AccordionNav menus={ADMIN_NAV_MENUS} path="/admin/" />
|
<AccordionNav menus={menus} path="/admin/" />
|
||||||
</Col>
|
</Col>
|
||||||
<Col lg={formPaths.find((v) => pathname.includes(v)) ? 6 : 10}>
|
<Col lg={g10Paths.find((v) => curPath.includes(v)) ? 10 : 6}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -71,6 +71,13 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
||||||
isInvalid: true,
|
isInvalid: true,
|
||||||
errorMsg: t('admin_name.msg'),
|
errorMsg: t('admin_name.msg'),
|
||||||
};
|
};
|
||||||
|
} else if (/[^a-z0-9\-._]/.test(name.value)) {
|
||||||
|
bol = false;
|
||||||
|
data.name = {
|
||||||
|
value: name.value,
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg: t('admin_name.character'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password.value) {
|
if (!password.value) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import * as Type from '@/common/interface';
|
||||||
|
|
||||||
const Questions: FC = () => {
|
const Questions: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||||
|
const { t: t2 } = useTranslation('translation');
|
||||||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||||
const [urlSearchParams] = useSearchParams();
|
const [urlSearchParams] = useSearchParams();
|
||||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||||
|
@ -46,8 +47,7 @@ const Questions: FC = () => {
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h5 className="card-title">
|
<h5 className="card-title">
|
||||||
{t('page_title', {
|
{t2('website_welcome', {
|
||||||
keyPrefix: 'login',
|
|
||||||
site_name: siteInfo.name,
|
site_name: siteInfo.name,
|
||||||
})}
|
})}
|
||||||
</h5>
|
</h5>
|
||||||
|
|
|
@ -3,12 +3,11 @@ import { Container, Row, Col } from 'react-bootstrap';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { siteInfoStore } from '@/stores';
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { WelcomeTitle } from '@/components';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
usePageTags({
|
usePageTags({
|
||||||
title: t('account_activation', { keyPrefix: 'page_title' }),
|
title: t('account_activation', { keyPrefix: 'page_title' }),
|
||||||
|
@ -17,9 +16,7 @@ const Index: FC = () => {
|
||||||
<Container className="pt-4 mt-2 mb-5">
|
<Container className="pt-4 mt-2 mb-5">
|
||||||
<Row className="justify-content-center">
|
<Row className="justify-content-center">
|
||||||
<Col lg={6}>
|
<Col lg={6}>
|
||||||
<h3 className="text-center mt-3 mb-5">
|
<WelcomeTitle className="mt-3 mb-5" />
|
||||||
{t('page_title', { site_name: siteName })}
|
|
||||||
</h3>
|
|
||||||
{location.pathname?.includes('success') && (
|
{location.pathname?.includes('success') && (
|
||||||
<>
|
<>
|
||||||
<p className="text-center">{t('success')}</p>
|
<p className="text-center">{t('success')}</p>
|
||||||
|
|
|
@ -2,22 +2,19 @@ import { FC, memo } from 'react';
|
||||||
import { Container, Col } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { siteInfoStore } from '@/stores';
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { WelcomeTitle } from '@/components';
|
||||||
|
|
||||||
import SendEmail from './components/sendEmail';
|
import SendEmail from './components/sendEmail';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
||||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
|
||||||
usePageTags({
|
usePageTags({
|
||||||
title: t('change_email', { keyPrefix: 'page_title' }),
|
title: t('change_email', { keyPrefix: 'page_title' }),
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||||
<h3 className="text-center mb-5">
|
<WelcomeTitle />
|
||||||
{t('page_title', { site_name: siteName })}
|
|
||||||
</h3>
|
|
||||||
<Col className="mx-auto" md={3}>
|
<Col className="mx-auto" md={3}>
|
||||||
<SendEmail />
|
<SendEmail />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
@ -11,12 +11,9 @@ import type {
|
||||||
ImgCodeRes,
|
ImgCodeRes,
|
||||||
FormDataType,
|
FormDataType,
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { Unactivate } from '@/components';
|
import { Unactivate, WelcomeTitle } from '@/components';
|
||||||
import {
|
import { PluginOauth } from '@/plugins';
|
||||||
loggedUserInfoStore,
|
import { loggedUserInfoStore, loginSettingStore } from '@/stores';
|
||||||
loginSettingStore,
|
|
||||||
siteInfoStore,
|
|
||||||
} from '@/stores';
|
|
||||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||||
import { login, checkImgCode } from '@/services';
|
import { login, checkImgCode } from '@/services';
|
||||||
import { PicAuthCodeModal } from '@/components/Modal';
|
import { PicAuthCodeModal } from '@/components/Modal';
|
||||||
|
@ -27,7 +24,6 @@ const Index: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [refresh, setRefresh] = useState(0);
|
const [refresh, setRefresh] = useState(0);
|
||||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
|
||||||
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
||||||
const loginSetting = loginSettingStore((state) => state.login);
|
const loginSetting = loginSettingStore((state) => state.login);
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
|
@ -165,7 +161,7 @@ const Index: React.FC = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isInactive = searchParams.get('status');
|
const isInactive = searchParams.get('status');
|
||||||
|
|
||||||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
if (storeUser.id && (storeUser.mail_status === 2 || isInactive)) {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -174,11 +170,10 @@ const Index: React.FC = () => {
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||||
<h3 className="text-center mb-5">
|
<WelcomeTitle />
|
||||||
{t('page_title', { site_name: siteName })}
|
|
||||||
</h3>
|
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<Col className="mx-auto" md={3}>
|
<Col className="mx-auto" md={3}>
|
||||||
|
<PluginOauth className="mb-5" />
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group controlId="email" className="mb-3">
|
<Form.Group controlId="email" className="mb-3">
|
||||||
<Form.Label>{t('email.label')}</Form.Label>
|
<Form.Label>{t('email.label')}</Form.Label>
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { FC, memo, useState, useEffect } from 'react';
|
||||||
|
import { Container, Col, Form, Button } from 'react-bootstrap';
|
||||||
|
import { useTranslation, Trans } from 'react-i18next';
|
||||||
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Modal, WelcomeTitle } from '@/components';
|
||||||
|
import type { FormDataType } from '@/common/interface';
|
||||||
|
import { usePageTags } from '@/hooks';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
import { oAuthBindEmail, getLoggedUserInfo } from '@/services';
|
||||||
|
import Storage from '@/utils/storage';
|
||||||
|
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||||
|
import { handleFormError } from '@/utils';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'oauth_bind_email',
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setUrlSearchParams] = useSearchParams();
|
||||||
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
|
const binding_key = searchParams.get('binding_key') || '';
|
||||||
|
const [showResult, setShowResult] = useState(false);
|
||||||
|
|
||||||
|
usePageTags({
|
||||||
|
title: t('confirm_email', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
|
email: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (params: FormDataType) => {
|
||||||
|
setFormData({ ...formData, ...params });
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkValidated = (): boolean => {
|
||||||
|
let bol = true;
|
||||||
|
|
||||||
|
if (!formData.email.value) {
|
||||||
|
bol = false;
|
||||||
|
formData.email = {
|
||||||
|
value: '',
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg: t('email.msg.empty'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
});
|
||||||
|
return bol;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserInfo = (token) => {
|
||||||
|
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
|
||||||
|
getLoggedUserInfo().then((user) => {
|
||||||
|
updateUser(user);
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/users/login?status=inactive', { replace: true });
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectConfirm = () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('modal_title'),
|
||||||
|
content: t('modal_content'),
|
||||||
|
cancelText: t('modal_cancel'),
|
||||||
|
confirmText: t('modal_confirm'),
|
||||||
|
onConfirm: () => {
|
||||||
|
// send activation email
|
||||||
|
oAuthBindEmail({
|
||||||
|
binding_key,
|
||||||
|
email: formData.email.value,
|
||||||
|
must: true,
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.access_token) {
|
||||||
|
getUserInfo(result.access_token);
|
||||||
|
} else {
|
||||||
|
searchParams.delete('binding_key');
|
||||||
|
setUrlSearchParams('');
|
||||||
|
setShowResult(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
setFormData({
|
||||||
|
email: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!checkValidated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binding_key) {
|
||||||
|
oAuthBindEmail({
|
||||||
|
binding_key,
|
||||||
|
email: formData.email.value,
|
||||||
|
must: false,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.email_exist_and_must_be_confirmed) {
|
||||||
|
connectConfirm();
|
||||||
|
}
|
||||||
|
if (res.access_token) {
|
||||||
|
getUserInfo(res.access_token);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!binding_key) {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||||
|
<WelcomeTitle />
|
||||||
|
{showResult ? (
|
||||||
|
<Col md={6} className="mx-auto text-center">
|
||||||
|
<p>
|
||||||
|
<Trans
|
||||||
|
i18nKey="inactive.first"
|
||||||
|
values={{ mail: formData.email.value }}
|
||||||
|
components={{ bold: <strong /> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p>{t('info', { keyPrefix: 'inactive' })}</p>
|
||||||
|
</Col>
|
||||||
|
) : (
|
||||||
|
<Col className="mx-auto" md={3}>
|
||||||
|
<div className="text-center mb-5">{t('subtitle')}</div>
|
||||||
|
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||||
|
<Form.Group controlId="email" className="mb-3">
|
||||||
|
<Form.Label>{t('email.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
value={formData.email.value}
|
||||||
|
isInvalid={formData.email.isInvalid}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleChange({
|
||||||
|
email: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.email.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<div className="d-grid mb-3">
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{t('btn_update')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { FC, memo, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { usePageTags, useLoginRedirect } from '@/hooks';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
import { getLoggedUserInfo } from '@/services';
|
||||||
|
import Storage from '@/utils/storage';
|
||||||
|
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||||
|
import { guard } from '@/utils';
|
||||||
|
|
||||||
|
const Index: FC = () => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { loginRedirect } = useLoginRedirect();
|
||||||
|
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get('access_token');
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
|
||||||
|
getLoggedUserInfo().then((res) => {
|
||||||
|
updateUser(res);
|
||||||
|
const userStat = guard.deriveLoginState();
|
||||||
|
if (userStat.isNotActivated) {
|
||||||
|
// inactive
|
||||||
|
navigate('/users/login?status=inactive', { replace: true });
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
loginRedirect();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
navigate('/', { replace: true });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
usePageTags({
|
||||||
|
title: t('oauth_callback'),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { FormEvent, MouseEvent, useEffect, useState } from 'react';
|
import React, { FormEvent, MouseEvent, useEffect, useState } from 'react';
|
||||||
import { Form, Button, Col } from 'react-bootstrap';
|
import { Form, Button } from 'react-bootstrap';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ import {
|
||||||
useLegalTos,
|
useLegalTos,
|
||||||
useLegalPrivacy,
|
useLegalPrivacy,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
import userStore from '@/stores/userInfo';
|
import userStore from '@/stores/loggedUserInfoStore';
|
||||||
import { handleFormError } from '@/utils';
|
import { handleFormError } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -70,6 +70,13 @@ const Index: React.FC<Props> = ({ callback }) => {
|
||||||
isInvalid: true,
|
isInvalid: true,
|
||||||
errorMsg: t('name.msg.empty'),
|
errorMsg: t('name.msg.empty'),
|
||||||
};
|
};
|
||||||
|
} else if (/[^a-z0-9\-._]/.test(name.value)) {
|
||||||
|
bol = false;
|
||||||
|
formData.name = {
|
||||||
|
value: name.value,
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg: t('name.msg.character'),
|
||||||
|
};
|
||||||
} else if ([...name.value].length > 30) {
|
} else if ([...name.value].length > 30) {
|
||||||
bol = false;
|
bol = false;
|
||||||
formData.name = {
|
formData.name = {
|
||||||
|
@ -170,112 +177,111 @@ const Index: React.FC<Props> = ({ callback }) => {
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Col className="mx-auto" md={3}>
|
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
<Form.Group controlId="name" className="mb-3">
|
||||||
<Form.Group controlId="name" className="mb-3">
|
<Form.Label>{t('name.label')}</Form.Label>
|
||||||
<Form.Label>{t('name.label')}</Form.Label>
|
<Form.Control
|
||||||
<Form.Control
|
autoComplete="off"
|
||||||
autoComplete="off"
|
required
|
||||||
required
|
type="text"
|
||||||
type="text"
|
isInvalid={formData.name.isInvalid}
|
||||||
isInvalid={formData.name.isInvalid}
|
value={formData.name.value}
|
||||||
value={formData.name.value}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
handleChange({
|
||||||
handleChange({
|
name: {
|
||||||
name: {
|
value: e.target.value,
|
||||||
value: e.target.value,
|
isInvalid: false,
|
||||||
isInvalid: false,
|
errorMsg: '',
|
||||||
errorMsg: '',
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Form.Control.Feedback type="invalid">
|
||||||
<Form.Control.Feedback type="invalid">
|
{formData.name.errorMsg}
|
||||||
{formData.name.errorMsg}
|
</Form.Control.Feedback>
|
||||||
</Form.Control.Feedback>
|
</Form.Group>
|
||||||
</Form.Group>
|
<Form.Group controlId="email" className="mb-3">
|
||||||
<Form.Group controlId="email" className="mb-3">
|
<Form.Label>{t('email.label')}</Form.Label>
|
||||||
<Form.Label>{t('email.label')}</Form.Label>
|
<Form.Control
|
||||||
<Form.Control
|
autoComplete="off"
|
||||||
autoComplete="off"
|
required
|
||||||
required
|
type="e_mail"
|
||||||
type="e_mail"
|
isInvalid={formData.e_mail.isInvalid}
|
||||||
isInvalid={formData.e_mail.isInvalid}
|
value={formData.e_mail.value}
|
||||||
value={formData.e_mail.value}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
handleChange({
|
||||||
handleChange({
|
e_mail: {
|
||||||
e_mail: {
|
value: e.target.value,
|
||||||
value: e.target.value,
|
isInvalid: false,
|
||||||
isInvalid: false,
|
errorMsg: '',
|
||||||
errorMsg: '',
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Form.Control.Feedback type="invalid">
|
||||||
<Form.Control.Feedback type="invalid">
|
{formData.e_mail.errorMsg}
|
||||||
{formData.e_mail.errorMsg}
|
</Form.Control.Feedback>
|
||||||
</Form.Control.Feedback>
|
</Form.Group>
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="password" className="mb-3">
|
<Form.Group controlId="password" className="mb-3">
|
||||||
<Form.Label>{t('password.label')}</Form.Label>
|
<Form.Label>{t('password.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
isInvalid={formData.pass.isInvalid}
|
isInvalid={formData.pass.isInvalid}
|
||||||
value={formData.pass.value}
|
value={formData.pass.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
pass: {
|
pass: {
|
||||||
value: e.target.value,
|
value: e.target.value,
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Control.Feedback type="invalid">
|
<Form.Control.Feedback type="invalid">
|
||||||
{formData.pass.errorMsg}
|
{formData.pass.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<div className="d-grid">
|
<div className="d-grid">
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit">
|
||||||
{t('signup', { keyPrefix: 'btns' })}
|
{t('signup', { keyPrefix: 'btns' })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
<div className="text-center fs-14 mt-3">
|
|
||||||
<Trans i18nKey="login.agreements" ns="translation">
|
|
||||||
By registering, you agree to the
|
|
||||||
<Link
|
|
||||||
to="/privacy"
|
|
||||||
onClick={(evt) => {
|
|
||||||
argumentClick(evt, 'privacy');
|
|
||||||
}}
|
|
||||||
target="_blank">
|
|
||||||
privacy policy
|
|
||||||
</Link>
|
|
||||||
and
|
|
||||||
<Link
|
|
||||||
to="/tos"
|
|
||||||
onClick={(evt) => {
|
|
||||||
argumentClick(evt, 'tos');
|
|
||||||
}}
|
|
||||||
target="_blank">
|
|
||||||
terms of service
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</Trans>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center mt-5">
|
</Form>
|
||||||
<Trans i18nKey="login.info_login" ns="translation">
|
<div className="text-center fs-14 mt-3">
|
||||||
Already have an account? <Link to="/users/login">Log in</Link>
|
<Trans i18nKey="login.agreements" ns="translation">
|
||||||
</Trans>
|
By registering, you agree to the
|
||||||
</div>
|
<Link
|
||||||
</Col>
|
to="/privacy"
|
||||||
|
onClick={(evt) => {
|
||||||
|
argumentClick(evt, 'privacy');
|
||||||
|
}}
|
||||||
|
target="_blank">
|
||||||
|
privacy policy
|
||||||
|
</Link>
|
||||||
|
and
|
||||||
|
<Link
|
||||||
|
to="/tos"
|
||||||
|
onClick={(evt) => {
|
||||||
|
argumentClick(evt, 'tos');
|
||||||
|
}}
|
||||||
|
target="_blank">
|
||||||
|
terms of service
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
<div className="text-center mt-5">
|
||||||
|
<Trans i18nKey="login.info_login" ns="translation">
|
||||||
|
Already have an account? <Link to="/users/login">Log in</Link>
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PicAuthCodeModal
|
<PicAuthCodeModal
|
||||||
visible={showModal}
|
visible={showModal}
|
||||||
data={{
|
data={{
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Container } from 'react-bootstrap';
|
import { Container, Col } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
import { Unactivate } from '@/components';
|
import { Unactivate, WelcomeTitle } from '@/components';
|
||||||
import { siteInfoStore } from '@/stores';
|
import { PluginOauth } from '@/plugins';
|
||||||
|
|
||||||
import SignUpForm from './components/SignUpForm';
|
import SignUpForm from './components/SignUpForm';
|
||||||
|
|
||||||
const Index: React.FC = () => {
|
const Index: React.FC = () => {
|
||||||
const [showForm, setShowForm] = useState(true);
|
const [showForm, setShowForm] = useState(true);
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
|
||||||
const onStep = () => {
|
const onStep = () => {
|
||||||
setShowForm((bol) => !bol);
|
setShowForm((bol) => !bol);
|
||||||
};
|
};
|
||||||
|
@ -20,11 +19,13 @@ const Index: React.FC = () => {
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||||
<h3 className="text-center mb-5">
|
<WelcomeTitle />
|
||||||
{t('page_title', { site_name: siteName })}
|
|
||||||
</h3>
|
|
||||||
{showForm ? (
|
{showForm ? (
|
||||||
<SignUpForm callback={onStep} />
|
<Col className="mx-auto" md={3}>
|
||||||
|
<PluginOauth className="mb-5" />
|
||||||
|
<SignUpForm callback={onStep} />
|
||||||
|
</Col>
|
||||||
) : (
|
) : (
|
||||||
<Unactivate visible={!showForm} />
|
<Unactivate visible={!showForm} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,20 +4,37 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import { getLoggedUserInfo, changeEmail } from '@/services';
|
import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services';
|
||||||
import { handleFormError } from '@/utils';
|
import { handleFormError } from '@/utils';
|
||||||
|
import { PicAuthCodeModal } from '@/components/Modal';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'settings.account',
|
keyPrefix: 'settings.account',
|
||||||
});
|
});
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
|
const [showModal, setModalState] = useState(false);
|
||||||
|
const [imgCode, setImgCode] = useState<Type.ImgCodeRes>({
|
||||||
|
captcha_id: '',
|
||||||
|
captcha_img: '',
|
||||||
|
verify: false,
|
||||||
|
});
|
||||||
const [formData, setFormData] = useState<Type.FormDataType>({
|
const [formData, setFormData] = useState<Type.FormDataType>({
|
||||||
e_mail: {
|
e_mail: {
|
||||||
value: '',
|
value: '',
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
|
pass: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
captcha_code: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
|
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
@ -27,13 +44,21 @@ const Index: FC = () => {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getImgCode = () => {
|
||||||
|
checkImgCode({
|
||||||
|
action: 'e_mail',
|
||||||
|
}).then((res) => {
|
||||||
|
setImgCode(res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (params: Type.FormDataType) => {
|
const handleChange = (params: Type.FormDataType) => {
|
||||||
setFormData({ ...formData, ...params });
|
setFormData({ ...formData, ...params });
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkValidated = (): boolean => {
|
const checkValidated = (): boolean => {
|
||||||
let bol = true;
|
let bol = true;
|
||||||
const { e_mail } = formData;
|
const { e_mail, pass } = formData;
|
||||||
|
|
||||||
if (!e_mail.value) {
|
if (!e_mail.value) {
|
||||||
bol = false;
|
bol = false;
|
||||||
|
@ -43,41 +68,86 @@ const Index: FC = () => {
|
||||||
errorMsg: t('email.msg'),
|
errorMsg: t('email.msg'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pass.value) {
|
||||||
|
bol = false;
|
||||||
|
formData.pass = {
|
||||||
|
value: '',
|
||||||
|
isInvalid: true,
|
||||||
|
errorMsg: t('pass.msg'),
|
||||||
|
};
|
||||||
|
}
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
});
|
});
|
||||||
return bol;
|
return bol;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initFormData = () => {
|
||||||
|
setFormData({
|
||||||
|
e_mail: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
pass: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
captcha_code: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const postEmail = () => {
|
||||||
|
const params: any = {
|
||||||
|
e_mail: formData.e_mail.value,
|
||||||
|
pass: formData.pass.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imgCode.verify) {
|
||||||
|
params.captcha_code = formData.captcha_code.value;
|
||||||
|
params.captcha_id = imgCode.captcha_id;
|
||||||
|
}
|
||||||
|
changeEmail(params)
|
||||||
|
.then(() => {
|
||||||
|
setStep(1);
|
||||||
|
setModalState(false);
|
||||||
|
toast.onShow({
|
||||||
|
msg: t('change_email_info'),
|
||||||
|
variant: 'warning',
|
||||||
|
});
|
||||||
|
initFormData();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData);
|
||||||
|
setFormData({ ...data });
|
||||||
|
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
|
||||||
|
setModalState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
getImgCode();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (event: FormEvent) => {
|
const handleSubmit = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!checkValidated()) {
|
if (!checkValidated()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
changeEmail({
|
|
||||||
e_mail: formData.e_mail.value,
|
if (imgCode.verify) {
|
||||||
})
|
setModalState(true);
|
||||||
.then(() => {
|
}
|
||||||
setStep(1);
|
postEmail();
|
||||||
toast.onShow({
|
|
||||||
msg: t('change_email_info'),
|
|
||||||
variant: 'warning',
|
|
||||||
});
|
|
||||||
setFormData({
|
|
||||||
e_mail: {
|
|
||||||
value: '',
|
|
||||||
isInvalid: false,
|
|
||||||
errorMsg: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.isError) {
|
|
||||||
const data = handleFormError(err, formData);
|
|
||||||
setFormData({ ...data });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -96,13 +166,41 @@ const Index: FC = () => {
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Button variant="outline-secondary" onClick={() => setStep(2)}>
|
<Button
|
||||||
|
variant="outline-secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setStep(2);
|
||||||
|
getImgCode();
|
||||||
|
}}>
|
||||||
{t('change_email_btn')}
|
{t('change_email_btn')}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Form.Group controlId="currentPass" className="mb-3">
|
||||||
|
<Form.Label>{t('pass.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
maxLength={32}
|
||||||
|
isInvalid={formData.pass.isInvalid}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleChange({
|
||||||
|
pass: {
|
||||||
|
value: e.target.value,
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.pass.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="newEmail" className="mb-3">
|
<Form.Group controlId="newEmail" className="mb-3">
|
||||||
<Form.Label>{t('email.label')}</Form.Label>
|
<Form.Label>{t('email.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -126,6 +224,7 @@ const Index: FC = () => {
|
||||||
{formData.e_mail.errorMsg}
|
{formData.e_mail.errorMsg}
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="submit" variant="primary" className="me-2">
|
<Button type="submit" variant="primary" className="me-2">
|
||||||
{t('save', { keyPrefix: 'btns' })}
|
{t('save', { keyPrefix: 'btns' })}
|
||||||
|
@ -137,6 +236,18 @@ const Index: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PicAuthCodeModal
|
||||||
|
visible={showModal}
|
||||||
|
data={{
|
||||||
|
captcha: formData.captcha_code,
|
||||||
|
imgCode,
|
||||||
|
}}
|
||||||
|
handleCaptcha={handleChange}
|
||||||
|
clickSubmit={postEmail}
|
||||||
|
refreshImgCode={getImgCode}
|
||||||
|
onClose={() => setModalState(false)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,15 +2,19 @@ import React, { FC, FormEvent, useState } from 'react';
|
||||||
import { Form, Button } from 'react-bootstrap';
|
import { Form, Button } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classname from 'classnames';
|
||||||
|
|
||||||
import { useToast } from '@/hooks';
|
import { useToast } from '@/hooks';
|
||||||
import type { FormDataType } from '@/common/interface';
|
import type { FormDataType } from '@/common/interface';
|
||||||
import { modifyPassword } from '@/services';
|
import { modifyPassword } from '@/services';
|
||||||
import { handleFormError } from '@/utils';
|
import { handleFormError } from '@/utils';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
keyPrefix: 'settings.account',
|
keyPrefix: 'settings.account',
|
||||||
});
|
});
|
||||||
|
const { user } = loggedUserInfoStore();
|
||||||
const [showForm, setFormState] = useState(false);
|
const [showForm, setFormState] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [formData, setFormData] = useState<FormDataType>({
|
const [formData, setFormData] = useState<FormDataType>({
|
||||||
|
@ -42,8 +46,7 @@ const Index: FC = () => {
|
||||||
const checkValidated = (): boolean => {
|
const checkValidated = (): boolean => {
|
||||||
let bol = true;
|
let bol = true;
|
||||||
const { old_pass, pass, pass2 } = formData;
|
const { old_pass, pass, pass2 } = formData;
|
||||||
|
if (!old_pass.value && user.have_password) {
|
||||||
if (!old_pass.value) {
|
|
||||||
bol = false;
|
bol = false;
|
||||||
formData.old_pass = {
|
formData.old_pass = {
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -130,14 +133,15 @@ const Index: FC = () => {
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
{showForm ? (
|
{showForm ? (
|
||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Form.Group controlId="oldPass" className="mb-3">
|
<Form.Group
|
||||||
|
controlId="oldPass"
|
||||||
|
className={classname('mb-3', user.have_password ? '' : 'd-none')}>
|
||||||
<Form.Label>{t('current_pass.label')}</Form.Label>
|
<Form.Label>{t('current_pass.label')}</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
// value={formData.password.value}
|
|
||||||
isInvalid={formData.old_pass.isInvalid}
|
isInvalid={formData.old_pass.isInvalid}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
|
@ -161,7 +165,6 @@ const Index: FC = () => {
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
// value={formData.password.value}
|
|
||||||
isInvalid={formData.pass.isInvalid}
|
isInvalid={formData.pass.isInvalid}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
|
@ -185,7 +188,6 @@ const Index: FC = () => {
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
maxLength={32}
|
maxLength={32}
|
||||||
// value={formData.password.value}
|
|
||||||
isInvalid={formData.pass2.isInvalid}
|
isInvalid={formData.pass2.isInvalid}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChange({
|
handleChange({
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Modal } from '@/components';
|
||||||
|
import { useOauthConnectorInfoByUser, userOauthUnbind } from '@/services';
|
||||||
|
import { useToast } from '@/hooks';
|
||||||
|
import { base64ToSvg } from '@/utils';
|
||||||
|
import Storage from '@/utils/storage';
|
||||||
|
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
const { data, mutate } = useOauthConnectorInfoByUser();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const { t } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'settings.my_logins',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t: t2 } = useTranslation('translation', {
|
||||||
|
keyPrefix: 'plugins.oauth',
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteLogins = (e, item) => {
|
||||||
|
if (!item.binding) {
|
||||||
|
Storage.set(REDIRECT_PATH_STORAGE_KEY, window.location.pathname);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('modal_title'),
|
||||||
|
content: t('modal_content'),
|
||||||
|
confirmBtnVariant: 'danger',
|
||||||
|
confirmText: t('modal_confirm_btn'),
|
||||||
|
onConfirm: () => {
|
||||||
|
userOauthUnbind({ external_id: item.external_id }).then(() => {
|
||||||
|
mutate();
|
||||||
|
toast.onShow({
|
||||||
|
msg: t('remove_success'),
|
||||||
|
variant: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!data?.length) return null;
|
||||||
|
return (
|
||||||
|
<div className="mt-5">
|
||||||
|
<div className="form-label">{t('title')}</div>
|
||||||
|
<small className="form-text mt-0">{t('label')}</small>
|
||||||
|
|
||||||
|
<div className="d-grid gap-2 mt-3">
|
||||||
|
{data?.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.name}>
|
||||||
|
<Button
|
||||||
|
variant={item.binding ? 'outline-danger' : 'outline-secondary'}
|
||||||
|
href={item.link}
|
||||||
|
onClick={(e) => deleteLogins(e, item)}>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: base64ToSvg(item.icon),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{t2(item.binding ? 'remove' : 'connect', {
|
||||||
|
auth_name: item.name,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -0,0 +1,5 @@
|
||||||
|
import ModifyEmail from './ModifyEmail';
|
||||||
|
import ModifyPassword from './ModifyPass';
|
||||||
|
import MyLogins from './MyLogins';
|
||||||
|
|
||||||
|
export { ModifyEmail, ModifyPassword, MyLogins };
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ModifyEmail from './components/ModifyEmail';
|
import { ModifyEmail, ModifyPassword, MyLogins } from './components';
|
||||||
import ModifyPassword from './components/ModifyPass';
|
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { t } = useTranslation('translation', {
|
const { t } = useTranslation('translation', {
|
||||||
|
@ -13,6 +12,7 @@ const Index = () => {
|
||||||
<h3 className="mb-4">{t('heading')}</h3>
|
<h3 className="mb-4">{t('heading')}</h3>
|
||||||
<ModifyEmail />
|
<ModifyEmail />
|
||||||
<ModifyPassword />
|
<ModifyPassword />
|
||||||
|
<MyLogins />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,6 @@ const Index = () => {
|
||||||
notice_switch: {
|
notice_switch: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
title: t('email.label'),
|
title: t('email.label'),
|
||||||
label: t('email.radio'),
|
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -25,6 +24,9 @@ const Index = () => {
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
notice_switch: {
|
notice_switch: {
|
||||||
'ui:widget': 'switch',
|
'ui:widget': 'switch',
|
||||||
|
'ui:options': {
|
||||||
|
label: t('email.radio'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { memo, FC } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { useGetStartUseOauthConnector } from '@/services';
|
||||||
|
import { base64ToSvg } from '@/utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
const Index: FC<Props> = ({ className }) => {
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||||
|
const { data } = useGetStartUseOauthConnector();
|
||||||
|
|
||||||
|
if (!data?.length) return null;
|
||||||
|
return (
|
||||||
|
<div className={classnames('d-grid gap-2', className)}>
|
||||||
|
{data?.map((item) => {
|
||||||
|
return (
|
||||||
|
<Button variant="outline-secondary" href={item.link} key={item.name}>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: base64ToSvg(item.icon),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>{t('connect', { auth_name: item.name })}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Index);
|
|
@ -0,0 +1,3 @@
|
||||||
|
import PluginOauth from './PluginOauth';
|
||||||
|
|
||||||
|
export { PluginOauth };
|
|
@ -26,6 +26,10 @@ const routes: RouteNode[] = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
page: 'pages/Layout',
|
page: 'pages/Layout',
|
||||||
|
loader: async () => {
|
||||||
|
await guard.setupApp();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
guard: () => {
|
guard: () => {
|
||||||
const gr = guard.shouldLoginRequired();
|
const gr = guard.shouldLoginRequired();
|
||||||
if (!gr.ok) {
|
if (!gr.ok) {
|
||||||
|
@ -223,6 +227,14 @@ const routes: RouteNode[] = [
|
||||||
return guard.forbidden();
|
return guard.forbidden();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/users/confirm-email',
|
||||||
|
page: 'pages/Users/OauthBindEmail',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users/oauth',
|
||||||
|
page: 'pages/Users/OauthCallback',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/posts/:qid/timeline',
|
path: '/posts/:qid/timeline',
|
||||||
page: 'pages/Timeline',
|
page: 'pages/Timeline',
|
||||||
|
@ -324,6 +336,14 @@ const routes: RouteNode[] = [
|
||||||
path: 'login',
|
path: 'login',
|
||||||
page: 'pages/Admin/Login',
|
page: 'pages/Admin/Login',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'installed_plugins',
|
||||||
|
page: 'pages/Admin/Plugins/Installed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':slug_name',
|
||||||
|
page: 'pages/Admin/Plugins/Config',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// for review
|
// for review
|
||||||
|
|
|
@ -4,3 +4,4 @@ export * from './question';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
export * from './dashboard';
|
export * from './dashboard';
|
||||||
|
export * from './plugins';
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import qs from 'qs';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import type * as Types from '@/common/interface';
|
||||||
|
import request from '@/utils/request';
|
||||||
|
|
||||||
|
export const useQueryPlugins = (params) => {
|
||||||
|
const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;
|
||||||
|
const { data, error, mutate } = useSWR<any[], Error>(
|
||||||
|
apiUrl,
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePluginStatus = (params) => {
|
||||||
|
return request.put('/answer/admin/api/plugin/status', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useQueryPluginConfig = (params) => {
|
||||||
|
const apiUrl = `/answer/admin/api/plugin/config?${qs.stringify(params)}`;
|
||||||
|
const { data, error, mutate } = useSWR<Types.PluginConfig, Error>(
|
||||||
|
apiUrl,
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePluginConfig = (params) => {
|
||||||
|
return request.put('/answer/admin/api/plugin/config', params);
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import request from '@/utils/request';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
|
||||||
|
export const oAuthBindEmail = (data: Type.OauthBindEmailReq) => {
|
||||||
|
return request.post('/answer/api/v1/connector/binding/email', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOauthConnectorInfoByUser = () => {
|
||||||
|
const { data, error, mutate } = useSWR<Type.UserOauthConnectorItem[]>(
|
||||||
|
'/answer/api/v1/connector/user/info',
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
mutate,
|
||||||
|
isLoading: !data && !error,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userOauthUnbind = (data: { external_id: string }) => {
|
||||||
|
return request.delete('/answer/api/v1/connector/user/unbinding', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetStartUseOauthConnector = () => {
|
||||||
|
const { data, error } = useSWR<Type.OauthConnectorItem[]>(
|
||||||
|
'/answer/api/v1/connector/info',
|
||||||
|
request.instance.get,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
|
@ -9,3 +9,4 @@ export * from './legal';
|
||||||
export * from './timeline';
|
export * from './timeline';
|
||||||
export * from './revision';
|
export * from './revision';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
|
export * from './Oauth';
|
||||||
|
|
|
@ -249,7 +249,7 @@ export const closeQuestion = (params: {
|
||||||
return request.put('/answer/api/v1/question/status', params);
|
return request.put('/answer/api/v1/question/status', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changeEmail = (params: { e_mail: string }) => {
|
export const changeEmail = (params: { e_mail: string; pass?: string }) => {
|
||||||
return request.post('/answer/api/v1/user/email/change/code', params);
|
return request.post('/answer/api/v1/user/email/change/code', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import loginSettingStore from '@/stores/loginSetting';
|
||||||
import seoSettingStore from '@/stores/seoSetting';
|
import seoSettingStore from '@/stores/seoSetting';
|
||||||
|
|
||||||
import toastStore from './toast';
|
import toastStore from './toast';
|
||||||
import loggedUserInfoStore from './userInfo';
|
import loggedUserInfoStore from './loggedUserInfoStore';
|
||||||
import siteInfoStore from './siteInfo';
|
import siteInfoStore from './siteInfo';
|
||||||
import interfaceStore from './interface';
|
import interfaceStore from './interface';
|
||||||
import brandingStore from './branding';
|
import brandingStore from './branding';
|
||||||
|
|
|
@ -26,12 +26,17 @@ const initUser: UserInfoRes = {
|
||||||
status: '',
|
status: '',
|
||||||
mail_status: 1,
|
mail_status: 1,
|
||||||
language: 'Default',
|
language: 'Default',
|
||||||
|
is_admin: false,
|
||||||
|
have_password: true,
|
||||||
role_id: 1,
|
role_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||||
user: initUser,
|
user: initUser,
|
||||||
update: (params) => {
|
update: (params) => {
|
||||||
|
if (typeof params !== 'object' || !params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!params?.language) {
|
if (!params?.language) {
|
||||||
params.language = 'Default';
|
params.language = 'Default';
|
||||||
}
|
}
|
|
@ -233,6 +233,26 @@ function diffText(newText: string, oldText?: string): string {
|
||||||
return result.join('');
|
return result.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function base64ToSvg(base64: string) {
|
||||||
|
// base64 to svg xml
|
||||||
|
const svgxml = atob(base64);
|
||||||
|
|
||||||
|
// svg add class btnSvg
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(svgxml, 'image/svg+xml');
|
||||||
|
const svg = doc.querySelector('svg');
|
||||||
|
let str = '';
|
||||||
|
if (svg) {
|
||||||
|
svg.classList.add('btnSvg');
|
||||||
|
svg.classList.add('me-2');
|
||||||
|
|
||||||
|
// transform svg to string
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
str = serializer.serializeToString(doc);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
thousandthDivision,
|
thousandthDivision,
|
||||||
formatCount,
|
formatCount,
|
||||||
|
@ -248,4 +268,5 @@ export {
|
||||||
labelStyle,
|
labelStyle,
|
||||||
handleFormError,
|
handleFormError,
|
||||||
diffText,
|
diffText,
|
||||||
|
base64ToSvg,
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,10 +11,10 @@ import {
|
||||||
loginToContinueStore,
|
loginToContinueStore,
|
||||||
} from '@/stores';
|
} from '@/stores';
|
||||||
import { RouteAlias } from '@/router/alias';
|
import { RouteAlias } from '@/router/alias';
|
||||||
import Storage from '@/utils/storage';
|
|
||||||
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
import { LOGGED_USER_STORAGE_KEY } from '@/common/constants';
|
||||||
import { setupAppLanguage, setupAppTimeZone } from '@/utils/localize';
|
|
||||||
|
|
||||||
|
import Storage from './storage';
|
||||||
|
import { setupAppLanguage, setupAppTimeZone } from './localize';
|
||||||
import { floppyNavigation } from './floppyNavigation';
|
import { floppyNavigation } from './floppyNavigation';
|
||||||
|
|
||||||
type TLoginState = {
|
type TLoginState = {
|
||||||
|
@ -98,7 +98,6 @@ export const pullLoggedUser = async (forceRePull = false) => {
|
||||||
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dedupeTimestamp = Date.now();
|
dedupeTimestamp = Date.now();
|
||||||
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
||||||
dedupeTimestamp = 0;
|
dedupeTimestamp = 0;
|
||||||
|
@ -298,6 +297,10 @@ export const tryLoggedAndActivated = () => {
|
||||||
return gr;
|
return gr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize app configuration
|
||||||
|
*/
|
||||||
|
let appInitialized = false;
|
||||||
export const initAppSettingsStore = async () => {
|
export const initAppSettingsStore = async () => {
|
||||||
const appSettings = await getAppSettings();
|
const appSettings = await getAppSettings();
|
||||||
if (appSettings) {
|
if (appSettings) {
|
||||||
|
@ -314,24 +317,18 @@ export const initAppSettingsStore = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldInitAppFetchData = () => {
|
|
||||||
if (isIgnoredPath('/install') && window.location.pathname === '/install') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupApp = async () => {
|
export const setupApp = async () => {
|
||||||
|
if (appInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* WARN:
|
* WARN:
|
||||||
* 1. must pre init logged user info for router guard
|
* 1. must pre init logged user info for router guard
|
||||||
* 2. must pre init app settings for app render
|
* 2. must pre init app settings for app render
|
||||||
*/
|
*/
|
||||||
// TODO: optimize `initAppSettingsStore` by server render
|
// TODO: optimize `initAppSettingsStore` by server render
|
||||||
if (shouldInitAppFetchData()) {
|
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
||||||
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
setupAppLanguage();
|
||||||
await setupAppLanguage();
|
setupAppTimeZone();
|
||||||
setupAppTimeZone();
|
appInitialized = true;
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ export { floppyNavigation } from './floppyNavigation';
|
||||||
export { default as storageExpires } from './storageWithExpires';
|
export { default as storageExpires } from './storageWithExpires';
|
||||||
export { default as SaveDraft } from './saveDraft';
|
export { default as SaveDraft } from './saveDraft';
|
||||||
|
|
||||||
export * as guard from './guard';
|
|
||||||
export * as localize from './localize';
|
|
||||||
export * from './common';
|
export * from './common';
|
||||||
export * from './color';
|
export * from './color';
|
||||||
|
export * as localize from './localize';
|
||||||
|
export * as guard from './guard';
|
||||||
|
|
|
@ -9,13 +9,14 @@ import {
|
||||||
DEFAULT_LANG,
|
DEFAULT_LANG,
|
||||||
LANG_RESOURCE_STORAGE_KEY,
|
LANG_RESOURCE_STORAGE_KEY,
|
||||||
} from '@/common/constants';
|
} from '@/common/constants';
|
||||||
import { Storage } from '@/utils';
|
|
||||||
import {
|
import {
|
||||||
getAdminLanguageOptions,
|
getAdminLanguageOptions,
|
||||||
getLanguageConfig,
|
getLanguageConfig,
|
||||||
getLanguageOptions,
|
getLanguageOptions,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
|
|
||||||
|
import Storage from './storage';
|
||||||
|
|
||||||
export const loadLanguageOptions = async (forAdmin = false) => {
|
export const loadLanguageOptions = async (forAdmin = false) => {
|
||||||
const languageOptions = forAdmin
|
const languageOptions = forAdmin
|
||||||
? await getAdminLanguageOptions()
|
? await getAdminLanguageOptions()
|
||||||
|
|
Loading…
Reference in New Issue