mirror of https://gitee.com/answerdev/answer.git
commit
5f113285a1
|
@ -322,6 +322,7 @@ ui:
|
|||
upgrade: Answer Upgrade
|
||||
maintenance: Website Maintenance
|
||||
users: Users
|
||||
oauth_callback: Processing
|
||||
http_404: HTTP Error 404
|
||||
http_50X: HTTP Error 500
|
||||
http_403: HTTP Error 403
|
||||
|
@ -658,7 +659,6 @@ ui:
|
|||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
|
@ -669,6 +669,7 @@ ui:
|
|||
msg:
|
||||
empty: Name cannot be empty.
|
||||
range: Name up to 30 characters.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
|
@ -689,7 +690,6 @@ ui:
|
|||
msg:
|
||||
empty: Email cannot be empty.
|
||||
change_email:
|
||||
page_title: Welcome to {{site_name}}
|
||||
btn_cancel: Cancel
|
||||
btn_update: Update email address
|
||||
send_success: >-
|
||||
|
@ -699,6 +699,17 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
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:
|
||||
page_title: Password Reset
|
||||
btn_name: Reset my password
|
||||
|
@ -770,6 +781,9 @@ ui:
|
|||
email:
|
||||
label: Email
|
||||
msg: Email cannot be empty.
|
||||
pass:
|
||||
label: Current Password
|
||||
msg: Password cannot be empty.
|
||||
password_title: Password
|
||||
current_pass:
|
||||
label: Current Password
|
||||
|
@ -786,6 +800,13 @@ ui:
|
|||
lang:
|
||||
label: Interface Language
|
||||
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:
|
||||
update: update success
|
||||
update_password: Password changed successfully.
|
||||
|
@ -907,7 +928,6 @@ ui:
|
|||
modal_confirm:
|
||||
title: Error...
|
||||
account_result:
|
||||
page_title: Welcome to {{site_name}}
|
||||
success: Your new account is confirmed; you will be redirected to the home page.
|
||||
link: Continue to homepage
|
||||
invalid: >-
|
||||
|
@ -1034,6 +1054,7 @@ ui:
|
|||
admin_name:
|
||||
label: Name
|
||||
msg: Name cannot be empty.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
admin_password:
|
||||
label: Password
|
||||
text: >-
|
||||
|
@ -1097,6 +1118,14 @@ ui:
|
|||
themes: Themes
|
||||
css-html: CSS/HTML
|
||||
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_header:
|
||||
title: Admin
|
||||
|
@ -1411,6 +1440,24 @@ ui:
|
|||
title: Private
|
||||
label: Login required
|
||||
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:
|
||||
optional: (optional)
|
||||
|
@ -1418,6 +1465,8 @@ ui:
|
|||
invalid: is invalid
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
select: Select
|
||||
|
||||
page_review:
|
||||
review: Review
|
||||
proposed: proposed
|
||||
|
|
|
@ -1053,6 +1053,7 @@ ui:
|
|||
themes: 主题
|
||||
css-html: CSS/HTML
|
||||
login: 登录
|
||||
website_welcome: 欢迎来到 {{site_name}}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
|
|
|
@ -92,10 +92,16 @@ export const ADMIN_NAV_MENUS = [
|
|||
{ name: 'login' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'plugins',
|
||||
children: [
|
||||
{
|
||||
name: 'installed_plugins',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ADMIN_LEGAL_MENUS = [{ name: 'tos' }, { name: 'privacy' }];
|
||||
|
||||
export const TIMEZONES = [
|
||||
{
|
||||
label: 'Africa',
|
||||
|
@ -585,7 +591,7 @@ export const TIMEZONES = [
|
|||
options: [{ value: 'UTC', label: 'UTC' }],
|
||||
},
|
||||
];
|
||||
export const DEFAULT_TIMEZONE = 'UTC+0';
|
||||
export const DEFAULT_TIMEZONE = 'UTC';
|
||||
|
||||
export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||
'undeleted',
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface FormValue<T = any> {
|
||||
value: T;
|
||||
isInvalid: boolean;
|
||||
|
@ -136,6 +138,7 @@ export interface UserInfoRes extends UserInfoBase {
|
|||
mail_status: number;
|
||||
language: string;
|
||||
e_mail?: string;
|
||||
have_password: boolean;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
|
@ -269,6 +272,11 @@ export type UserFilterBy =
|
|||
| 'suspended'
|
||||
| 'deleted';
|
||||
|
||||
export type InstalledPluginsFilterBy =
|
||||
| 'all'
|
||||
| 'active'
|
||||
| 'inactive'
|
||||
| 'outdated';
|
||||
/**
|
||||
* @description interface for Flags
|
||||
*/
|
||||
|
@ -527,6 +535,44 @@ export interface User {
|
|||
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 {
|
||||
id: string;
|
||||
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
||||
|
|
|
@ -21,7 +21,7 @@ function MenuNode({
|
|||
const href = isLeaf ? `${path}${menu.name}` : '#';
|
||||
|
||||
return (
|
||||
<Nav.Item key={menu.name}>
|
||||
<Nav.Item key={menu.name} className="w-100">
|
||||
<Nav.Link
|
||||
eventKey={menu.name}
|
||||
as={isLeaf ? 'a' : 'button'}
|
||||
|
@ -33,7 +33,9 @@ function MenuNode({
|
|||
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
||||
{ 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 ? (
|
||||
<span className="badge text-bg-dark">{menu.badgeContent}</span>
|
||||
) : null}
|
||||
|
@ -114,7 +116,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
};
|
||||
useEffect(() => {
|
||||
setOpenKey(getOpenKey());
|
||||
}, [activeKey]);
|
||||
}, [activeKey, menus]);
|
||||
return (
|
||||
<Accordion activeKey={openKey} flush>
|
||||
<Nav variant="pills" className="flex-column" activeKey={activeKey}>
|
||||
|
|
|
@ -18,12 +18,16 @@ const Index = ({
|
|||
title = '',
|
||||
confirmText = '',
|
||||
content,
|
||||
onCancel: onClose,
|
||||
onConfirm,
|
||||
cancelBtnVariant = 'link',
|
||||
confirmBtnVariant = 'primary',
|
||||
...props
|
||||
}: Config) => {
|
||||
const onCancel = () => {
|
||||
if (typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
render({ visible: false });
|
||||
div.remove();
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { loginToContinueStore, siteInfoStore } from '@/stores';
|
||||
import { WelcomeTitle } from '@/components';
|
||||
|
||||
interface IProps {
|
||||
visible: boolean;
|
||||
|
@ -32,7 +33,7 @@ const Index: React.FC<IProps> = ({ visible = false }) => {
|
|||
</Modal.Header>
|
||||
<Modal.Body className="p-5">
|
||||
<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>
|
||||
</div>
|
||||
<div className="d-grid gap-2">
|
||||
|
|
|
@ -51,7 +51,7 @@ const Index: React.FC<IProps> = ({
|
|||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder={t('placeholder')}
|
||||
isInvalid={captcha.isInvalid}
|
||||
isInvalid={captcha?.isInvalid}
|
||||
onChange={(e) => {
|
||||
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||
handleCaptcha({
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
# SchemaForm User Guide
|
||||
---
|
||||
sidebar_position: 0
|
||||
---
|
||||
# Schema Form
|
||||
|
||||
## 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
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
||||
|
||||
const schema: JSONSchema = {
|
||||
type: 'object',
|
||||
title: 'General',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
|
@ -23,12 +25,12 @@ const schema: JSONSchema = {
|
|||
type: 'number',
|
||||
title: 'Age',
|
||||
},
|
||||
sex:{
|
||||
type: 'boolean',
|
||||
sex: {
|
||||
type: 'string',
|
||||
title: 'sex',
|
||||
enum: [1, 2],
|
||||
enumNames: ['male', 'female'],
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -39,41 +41,193 @@ const uiSchema: UISchema = {
|
|||
age: {
|
||||
'ui:widget': 'input',
|
||||
'ui:options': {
|
||||
type: 'number'
|
||||
}
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
sex: {
|
||||
'ui:widget': 'radio',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// form component
|
||||
|
||||
const Form = () => {
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const handleChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={console.log}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ---------------------------------------- | ----------------------------------------- | ------- |
|
||||
| schema | JSON schema | [JSONSchema](index.tsx#L9) | - |
|
||||
| uiSchema | UI schema | [UISchema](index.tsx#L24) | - |
|
||||
| formData | Form data | [FormData](index.tsx#L66) | - |
|
||||
| onChange | Callback function when form data changes | (data: [FormData](index.tsx#L66)) => void | - |
|
||||
| -------- | ---------------------------------------- | ------------------------------------- | ------- |
|
||||
| schema | Describe the form structure with schema | [JSONSchema](#json-schema) | - |
|
||||
| uiSchema | Describe the properties of the field | [UISchema](#uischema) | - |
|
||||
| formData | Describe form data | [FormData](#formdata) | - |
|
||||
| onChange | Callback function when form data changes | (data: [FormData](#formdata)) => 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
|
||||
|
||||
- [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/)
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { Form, Button, Stack } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -20,7 +21,6 @@ export interface JSONSchema {
|
|||
[key: string]: {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
|
@ -28,21 +28,18 @@ export interface JSONSchema {
|
|||
};
|
||||
};
|
||||
}
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?:
|
||||
| 'textarea'
|
||||
| 'text'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch';
|
||||
'ui:options'?: {
|
||||
rows?: number;
|
||||
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
className?: string | string[];
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
}
|
||||
export interface InputOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
type?:
|
||||
inputType?:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
|
@ -57,16 +54,53 @@ export interface UISchema {
|
|||
| 'time'
|
||||
| 'url'
|
||||
| 'week';
|
||||
empty?: string;
|
||||
className?: string | string[];
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
textRender?: () => React.ReactElement;
|
||||
imageType?: Type.UploadType;
|
||||
}
|
||||
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 {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -116,6 +150,30 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
}
|
||||
|
||||
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 { name, value } = e.target;
|
||||
|
@ -265,16 +323,17 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (
|
||||
const handleInputCheck = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number,
|
||||
) => {
|
||||
const { name } = e.target;
|
||||
const { name, checked } = e.currentTarget;
|
||||
const freshVal = checked ? schema.properties[name]?.enum?.[index] : '';
|
||||
const data = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value: schema.properties[name]?.enum?.[index],
|
||||
value: freshVal,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
|
@ -286,12 +345,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
useImperativeHandle(ref, () => ({
|
||||
validator,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{keys.map((key) => {
|
||||
const { title, description, label } = properties[key];
|
||||
const { 'ui:widget': widget = 'input', 'ui:options': options = {} } =
|
||||
const { title, description } = properties[key];
|
||||
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||
uiSchema[key] || {};
|
||||
if (widget === 'select') {
|
||||
return (
|
||||
|
@ -303,7 +361,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
<Form.Select
|
||||
aria-label={description}
|
||||
name={key}
|
||||
value={formData[key]?.value}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleSelectChange}
|
||||
isInvalid={formData[key].isInvalid}>
|
||||
{properties[key].enum?.map((item, index) => {
|
||||
|
@ -323,6 +381,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (widget === 'checkbox' || widget === 'radio') {
|
||||
return (
|
||||
<Form.Group
|
||||
|
@ -341,11 +400,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
name={key}
|
||||
id={`form-${String(item)}`}
|
||||
label={properties[key].enumNames?.[index]}
|
||||
checked={formData[key]?.value === item}
|
||||
checked={(formData[key]?.value || '') === item}
|
||||
feedback={formData[key]?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
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}`}
|
||||
name={key}
|
||||
type="switch"
|
||||
label={label}
|
||||
checked={formData[key]?.value}
|
||||
label={(uiOpt as SwitchOptions)?.label}
|
||||
checked={formData[key]?.value || ''}
|
||||
feedback={formData[key]?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
|
@ -396,7 +455,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
controlId={key}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<TimeZonePicker
|
||||
value={formData[key]?.value}
|
||||
value={formData[key]?.value || ''}
|
||||
name={key}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
|
@ -416,6 +475,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
}
|
||||
|
||||
if (widget === 'upload') {
|
||||
const options: UploadOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
|
@ -444,6 +504,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
}
|
||||
|
||||
if (widget === 'textarea') {
|
||||
const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
|
||||
return (
|
||||
<Form.Group
|
||||
controlId={`form-${key}`}
|
||||
|
@ -454,8 +516,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
as="textarea"
|
||||
name={key}
|
||||
placeholder={options?.placeholder || ''}
|
||||
type={options?.type || 'text'}
|
||||
value={formData[key]?.value}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleInputChange}
|
||||
isInvalid={formData[key].isInvalid}
|
||||
rows={options?.rows || 3}
|
||||
|
@ -471,6 +532,9 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
const options: InputOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
|
||||
return (
|
||||
<Form.Group
|
||||
controlId={key}
|
||||
|
@ -480,10 +544,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
<Form.Control
|
||||
name={key}
|
||||
placeholder={options?.placeholder || ''}
|
||||
type={options?.type || 'text'}
|
||||
value={formData[key]?.value}
|
||||
type={options?.inputType || 'text'}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleInputChange}
|
||||
style={options?.type === 'color' ? { width: '6rem' } : {}}
|
||||
style={options?.inputType === 'color' ? { width: '6rem' } : {}}
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
|
@ -507,10 +571,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||
const formData: Type.FormDataType = {};
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
const v = schema.properties[key]?.default;
|
||||
// TODO: set default value by property type
|
||||
const prop = schema.properties[key];
|
||||
const defaultVal = prop?.default;
|
||||
formData[key] = {
|
||||
value: typeof v !== 'undefined' ? v : '',
|
||||
value: defaultVal,
|
||||
isInvalid: false,
|
||||
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 QuestionListLoader from './QuestionListLoader';
|
||||
import TagsLoader from './TagsLoader';
|
||||
import WelcomeTitle from './WelcomeTitle';
|
||||
import Counts from './Counts';
|
||||
import QuestionList from './QuestionList';
|
||||
import HotQuestions from './HotQuestions';
|
||||
|
@ -74,6 +75,7 @@ export {
|
|||
PageTags,
|
||||
QuestionListLoader,
|
||||
TagsLoader,
|
||||
WelcomeTitle,
|
||||
Counts,
|
||||
QuestionList,
|
||||
HotQuestions,
|
||||
|
|
|
@ -8,6 +8,7 @@ import useChangeUserRoleModal from './useChangeUserRoleModal';
|
|||
import useUserModal from './useUserModal';
|
||||
import useChangePasswordModal from './useChangePasswordModal';
|
||||
import usePageTags from './usePageTags';
|
||||
import useLoginRedirect from './useLoginRedirect';
|
||||
import usePromptWithUnload from './usePrompt';
|
||||
import useImgViewer from './useImgViewer';
|
||||
|
||||
|
@ -22,6 +23,7 @@ export {
|
|||
useUserModal,
|
||||
useChangePasswordModal,
|
||||
usePageTags,
|
||||
useLoginRedirect,
|
||||
usePromptWithUnload,
|
||||
useImgViewer,
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ const useChangePasswordModal = (props: IProps = {}) => {
|
|||
const uiSchema: UISchema = {
|
||||
password: {
|
||||
'ui:options': {
|
||||
type: 'password',
|
||||
inputType: 'password',
|
||||
validator: (value) => {
|
||||
const MIN_LENGTH = 8;
|
||||
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: {
|
||||
'ui:options': {
|
||||
type: 'email',
|
||||
inputType: 'email',
|
||||
validator: (value) => {
|
||||
if (value && !pattern.email.test(value)) {
|
||||
return t('form.fields.email.msg');
|
||||
|
@ -68,7 +68,7 @@ const useAddUserModal = (props: IProps = {}) => {
|
|||
},
|
||||
password: {
|
||||
'ui:options': {
|
||||
type: 'password',
|
||||
inputType: 'password',
|
||||
validator: (value) => {
|
||||
const MIN_LENGTH = 8;
|
||||
const MAX_LENGTH = 32;
|
||||
|
|
|
@ -315,3 +315,13 @@ img:not(a img, img.broken) {
|
|||
.bg-fade-out {
|
||||
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 { guard } from '@/utils';
|
||||
|
||||
import App from './App';
|
||||
|
||||
import './index.scss';
|
||||
|
@ -12,13 +10,8 @@ const root = ReactDOM.createRoot(
|
|||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
|
||||
async function bootstrapApp() {
|
||||
await guard.setupApp();
|
||||
root.render(
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
|
||||
bootstrapApp();
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import { FC } from 'react';
|
|||
import { Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { useDashBoard } from '@/services';
|
||||
|
||||
import {
|
||||
|
@ -13,6 +14,7 @@ import {
|
|||
|
||||
const Dashboard: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' });
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const { data } = useDashBoard();
|
||||
|
||||
if (!data) {
|
||||
|
@ -22,7 +24,7 @@ const Dashboard: FC = () => {
|
|||
return (
|
||||
<>
|
||||
<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>
|
||||
<Col lg={6}>
|
||||
<Statistics data={data.info} />
|
||||
|
|
|
@ -55,7 +55,7 @@ const General: FC = () => {
|
|||
const uiSchema: UISchema = {
|
||||
site_url: {
|
||||
'ui:options': {
|
||||
type: 'url',
|
||||
inputType: 'url',
|
||||
validator: (value) => {
|
||||
let url: URL | undefined;
|
||||
try {
|
||||
|
@ -79,7 +79,7 @@ const General: FC = () => {
|
|||
},
|
||||
contact_email: {
|
||||
'ui:options': {
|
||||
type: 'email',
|
||||
inputType: 'email',
|
||||
validator: (value) => {
|
||||
if (!Pattern.email.test(value)) {
|
||||
return t('contact_email.validate');
|
||||
|
|
|
@ -40,11 +40,13 @@ const Interface: FC = () => {
|
|||
description: t('language.text'),
|
||||
enum: langs?.map((lang) => lang.value),
|
||||
enumNames: langs?.map((lang) => lang.label),
|
||||
default: setting?.language || storeInterface.language,
|
||||
},
|
||||
time_zone: {
|
||||
type: 'string',
|
||||
title: t('time_zone.label'),
|
||||
description: t('time_zone.text'),
|
||||
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||
},
|
||||
default_avatar: {
|
||||
type: 'string',
|
||||
|
|
|
@ -19,14 +19,12 @@ const Index: FC = () => {
|
|||
allow_new_registrations: {
|
||||
type: 'boolean',
|
||||
title: t('membership.title'),
|
||||
label: t('membership.label'),
|
||||
description: t('membership.text'),
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
login_required: {
|
||||
type: 'boolean',
|
||||
title: t('private.title'),
|
||||
label: t('private.label'),
|
||||
description: t('private.text'),
|
||||
default: false,
|
||||
},
|
||||
|
@ -35,9 +33,15 @@ const Index: FC = () => {
|
|||
const uiSchema: UISchema = {
|
||||
allow_new_registrations: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('membership.label'),
|
||||
},
|
||||
},
|
||||
login_required: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('private.label'),
|
||||
},
|
||||
},
|
||||
};
|
||||
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'),
|
||||
},
|
||||
encryption: {
|
||||
type: 'boolean',
|
||||
type: 'string',
|
||||
title: t('encryption.label'),
|
||||
description: t('encryption.text'),
|
||||
enum: ['SSL', ''],
|
||||
|
@ -47,7 +47,6 @@ const Smtp: FC = () => {
|
|||
smtp_authentication: {
|
||||
type: 'boolean',
|
||||
title: t('smtp_authentication.title'),
|
||||
label: t('smtp_authentication.label'),
|
||||
enum: [true, false],
|
||||
enumNames: [t('smtp_authentication.yes'), t('smtp_authentication.no')],
|
||||
},
|
||||
|
@ -69,7 +68,7 @@ const Smtp: FC = () => {
|
|||
const uiSchema: UISchema = {
|
||||
from_email: {
|
||||
'ui:options': {
|
||||
type: 'email',
|
||||
inputType: 'email',
|
||||
},
|
||||
},
|
||||
encryption: {
|
||||
|
@ -89,7 +88,7 @@ const Smtp: FC = () => {
|
|||
},
|
||||
smtp_password: {
|
||||
'ui:options': {
|
||||
type: 'password',
|
||||
inputType: 'password',
|
||||
validator: (value: string, formData) => {
|
||||
if (formData.smtp_authentication.value) {
|
||||
if (!value) {
|
||||
|
@ -102,10 +101,13 @@ const Smtp: FC = () => {
|
|||
},
|
||||
smtp_authentication: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('smtp_authentication.label'),
|
||||
},
|
||||
},
|
||||
smtp_port: {
|
||||
'ui:options': {
|
||||
type: 'number',
|
||||
inputType: 'number',
|
||||
validator: (value) => {
|
||||
if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 65535) {
|
||||
return t('smtp_port.msg');
|
||||
|
@ -116,7 +118,7 @@ const Smtp: FC = () => {
|
|||
},
|
||||
test_email_recipient: {
|
||||
'ui:options': {
|
||||
type: 'email',
|
||||
inputType: 'email',
|
||||
validator: (value) => {
|
||||
if (value && !pattern.email.test(value)) {
|
||||
return t('test_email_recipient.msg');
|
||||
|
@ -177,7 +179,7 @@ const Smtp: FC = () => {
|
|||
}, [setting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.smtp_authentication.value === '') {
|
||||
if (!/true|false/.test(formData.smtp_authentication.value)) {
|
||||
return;
|
||||
}
|
||||
if (formData.smtp_authentication.value) {
|
||||
|
|
|
@ -50,7 +50,7 @@ const Index: FC = () => {
|
|||
},
|
||||
primary_color: {
|
||||
'ui:options': {
|
||||
type: 'color',
|
||||
inputType: 'color',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -27,7 +27,6 @@ const Index: FC = () => {
|
|||
required_tag: {
|
||||
type: 'boolean',
|
||||
title: t('required_tag.title'),
|
||||
label: t('required_tag.label'),
|
||||
description: t('required_tag.text'),
|
||||
},
|
||||
reserved_tags: {
|
||||
|
@ -46,6 +45,9 @@ const Index: FC = () => {
|
|||
},
|
||||
required_tag: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('required_tag.label'),
|
||||
},
|
||||
},
|
||||
reserved_tags: {
|
||||
'ui:widget': 'textarea',
|
||||
|
|
|
@ -1,29 +1,68 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
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 { AccordionNav } from '@/components';
|
||||
import { ADMIN_NAV_MENUS } from '@/common/constants';
|
||||
import { useQueryPlugins } from '@/services';
|
||||
import { interfaceStore } from '@/stores';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const formPaths = [
|
||||
'general',
|
||||
'smtp',
|
||||
'interface',
|
||||
'branding',
|
||||
'legal',
|
||||
'write',
|
||||
'seo',
|
||||
'themes',
|
||||
'css-html',
|
||||
const g10Paths = [
|
||||
'dashboard',
|
||||
'questions',
|
||||
'answers',
|
||||
'users',
|
||||
'flags',
|
||||
'installed_plugins',
|
||||
];
|
||||
|
||||
const Index: FC = () => {
|
||||
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({
|
||||
title: t('admin'),
|
||||
});
|
||||
|
@ -39,9 +78,9 @@ const Index: FC = () => {
|
|||
<Container className="admin-container">
|
||||
<Row>
|
||||
<Col lg={2}>
|
||||
<AccordionNav menus={ADMIN_NAV_MENUS} path="/admin/" />
|
||||
<AccordionNav menus={menus} path="/admin/" />
|
||||
</Col>
|
||||
<Col lg={formPaths.find((v) => pathname.includes(v)) ? 6 : 10}>
|
||||
<Col lg={g10Paths.find((v) => curPath.includes(v)) ? 10 : 6}>
|
||||
<Outlet />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -71,6 +71,13 @@ const Index: FC<Props> = ({ visible, data, changeCallback, nextCallback }) => {
|
|||
isInvalid: true,
|
||||
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) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import * as Type from '@/common/interface';
|
|||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { t: t2 } = useTranslation('translation');
|
||||
const { user: loggedUser } = loggedUserInfoStore((_) => _);
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
|
@ -46,8 +47,7 @@ const Questions: FC = () => {
|
|||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">
|
||||
{t('page_title', {
|
||||
keyPrefix: 'login',
|
||||
{t2('website_welcome', {
|
||||
site_name: siteInfo.name,
|
||||
})}
|
||||
</h5>
|
||||
|
|
|
@ -3,12 +3,11 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { WelcomeTitle } from '@/components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const location = useLocation();
|
||||
usePageTags({
|
||||
title: t('account_activation', { keyPrefix: 'page_title' }),
|
||||
|
@ -17,9 +16,7 @@ const Index: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={6}>
|
||||
<h3 className="text-center mt-3 mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
<WelcomeTitle className="mt-3 mb-5" />
|
||||
{location.pathname?.includes('success') && (
|
||||
<>
|
||||
<p className="text-center">{t('success')}</p>
|
||||
|
|
|
@ -2,22 +2,19 @@ import { FC, memo } from 'react';
|
|||
import { Container, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { WelcomeTitle } from '@/components';
|
||||
|
||||
import SendEmail from './components/sendEmail';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
usePageTags({
|
||||
title: t('change_email', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
<WelcomeTitle />
|
||||
<Col className="mx-auto" md={3}>
|
||||
<SendEmail />
|
||||
</Col>
|
||||
|
|
|
@ -11,12 +11,9 @@ import type {
|
|||
ImgCodeRes,
|
||||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
import { Unactivate } from '@/components';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
siteInfoStore,
|
||||
} from '@/stores';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { loggedUserInfoStore, loginSettingStore } from '@/stores';
|
||||
import { guard, floppyNavigation, handleFormError } from '@/utils';
|
||||
import { login, checkImgCode } from '@/services';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
|
@ -27,7 +24,6 @@ const Index: React.FC = () => {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
|
@ -165,7 +161,7 @@ const Index: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const isInactive = searchParams.get('status');
|
||||
|
||||
if ((storeUser.id && storeUser.mail_status === 2) || isInactive) {
|
||||
if (storeUser.id && (storeUser.mail_status === 2 || isInactive)) {
|
||||
setStep(2);
|
||||
}
|
||||
}, []);
|
||||
|
@ -174,11 +170,10 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
<WelcomeTitle />
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<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 { Form, Button, Col } from 'react-bootstrap';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -12,7 +12,7 @@ import {
|
|||
useLegalTos,
|
||||
useLegalPrivacy,
|
||||
} from '@/services';
|
||||
import userStore from '@/stores/userInfo';
|
||||
import userStore from '@/stores/loggedUserInfoStore';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
@ -70,6 +70,13 @@ const Index: React.FC<Props> = ({ callback }) => {
|
|||
isInvalid: true,
|
||||
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) {
|
||||
bol = false;
|
||||
formData.name = {
|
||||
|
@ -170,7 +177,6 @@ const Index: React.FC<Props> = ({ callback }) => {
|
|||
}, []);
|
||||
return (
|
||||
<>
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||
<Form.Group controlId="name" className="mb-3">
|
||||
<Form.Label>{t('name.label')}</Form.Label>
|
||||
|
@ -275,7 +281,7 @@ const Index: React.FC<Props> = ({ callback }) => {
|
|||
Already have an account? <Link to="/users/login">Log in</Link>
|
||||
</Trans>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<PicAuthCodeModal
|
||||
visible={showModal}
|
||||
data={{
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import { Container, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate } from '@/components';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const [showForm, setShowForm] = useState(true);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
const { name: siteName } = siteInfoStore((_) => _.siteInfo);
|
||||
const onStep = () => {
|
||||
setShowForm((bol) => !bol);
|
||||
};
|
||||
|
@ -20,11 +19,13 @@ const Index: React.FC = () => {
|
|||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
<WelcomeTitle />
|
||||
|
||||
{showForm ? (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<SignUpForm callback={onStep} />
|
||||
</Col>
|
||||
) : (
|
||||
<Unactivate visible={!showForm} />
|
||||
)}
|
||||
|
|
|
@ -4,20 +4,37 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { getLoggedUserInfo, changeEmail } from '@/services';
|
||||
import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.account',
|
||||
});
|
||||
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>({
|
||||
e_mail: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
pass: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
captcha_code: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
|
||||
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) => {
|
||||
setFormData({ ...formData, ...params });
|
||||
};
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { e_mail } = formData;
|
||||
const { e_mail, pass } = formData;
|
||||
|
||||
if (!e_mail.value) {
|
||||
bol = false;
|
||||
|
@ -43,41 +68,86 @@ const Index: FC = () => {
|
|||
errorMsg: t('email.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!pass.value) {
|
||||
bol = false;
|
||||
formData.pass = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('pass.msg'),
|
||||
};
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
});
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
changeEmail({
|
||||
e_mail: formData.e_mail.value,
|
||||
})
|
||||
.then(() => {
|
||||
setStep(1);
|
||||
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 });
|
||||
|
||||
if (imgCode.verify) {
|
||||
setModalState(true);
|
||||
}
|
||||
});
|
||||
postEmail();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -96,13 +166,41 @@ const Index: FC = () => {
|
|||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="outline-secondary" onClick={() => setStep(2)}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={() => {
|
||||
setStep(2);
|
||||
getImgCode();
|
||||
}}>
|
||||
{t('change_email_btn')}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<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.Label>{t('email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
|
@ -126,6 +224,7 @@ const Index: FC = () => {
|
|||
{formData.e_mail.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<div>
|
||||
<Button type="submit" variant="primary" className="me-2">
|
||||
{t('save', { keyPrefix: 'btns' })}
|
||||
|
@ -137,6 +236,18 @@ const Index: FC = () => {
|
|||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<PicAuthCodeModal
|
||||
visible={showModal}
|
||||
data={{
|
||||
captcha: formData.captcha_code,
|
||||
imgCode,
|
||||
}}
|
||||
handleCaptcha={handleChange}
|
||||
clickSubmit={postEmail}
|
||||
refreshImgCode={getImgCode}
|
||||
onClose={() => setModalState(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,15 +2,19 @@ import React, { FC, FormEvent, useState } from 'react';
|
|||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classname from 'classnames';
|
||||
|
||||
import { useToast } from '@/hooks';
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { modifyPassword } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.account',
|
||||
});
|
||||
const { user } = loggedUserInfoStore();
|
||||
const [showForm, setFormState] = useState(false);
|
||||
const toast = useToast();
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
|
@ -42,8 +46,7 @@ const Index: FC = () => {
|
|||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { old_pass, pass, pass2 } = formData;
|
||||
|
||||
if (!old_pass.value) {
|
||||
if (!old_pass.value && user.have_password) {
|
||||
bol = false;
|
||||
formData.old_pass = {
|
||||
value: '',
|
||||
|
@ -130,14 +133,15 @@ const Index: FC = () => {
|
|||
<div className="mt-5">
|
||||
{showForm ? (
|
||||
<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.Control
|
||||
autoComplete="off"
|
||||
required
|
||||
type="password"
|
||||
placeholder=""
|
||||
// value={formData.password.value}
|
||||
isInvalid={formData.old_pass.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
|
@ -161,7 +165,6 @@ const Index: FC = () => {
|
|||
required
|
||||
type="password"
|
||||
maxLength={32}
|
||||
// value={formData.password.value}
|
||||
isInvalid={formData.pass.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
|
@ -185,7 +188,6 @@ const Index: FC = () => {
|
|||
required
|
||||
type="password"
|
||||
maxLength={32}
|
||||
// value={formData.password.value}
|
||||
isInvalid={formData.pass2.isInvalid}
|
||||
onChange={(e) =>
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
|
||||
import ModifyEmail from './components/ModifyEmail';
|
||||
import ModifyPassword from './components/ModifyPass';
|
||||
import { ModifyEmail, ModifyPassword, MyLogins } from './components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
|
@ -13,6 +12,7 @@ const Index = () => {
|
|||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,6 @@ const Index = () => {
|
|||
notice_switch: {
|
||||
type: 'boolean',
|
||||
title: t('email.label'),
|
||||
label: t('email.radio'),
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
@ -25,6 +24,9 @@ const Index = () => {
|
|||
const uiSchema: UISchema = {
|
||||
notice_switch: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('email.radio'),
|
||||
},
|
||||
},
|
||||
};
|
||||
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: '/',
|
||||
page: 'pages/Layout',
|
||||
loader: async () => {
|
||||
await guard.setupApp();
|
||||
return null;
|
||||
},
|
||||
guard: () => {
|
||||
const gr = guard.shouldLoginRequired();
|
||||
if (!gr.ok) {
|
||||
|
@ -223,6 +227,14 @@ const routes: RouteNode[] = [
|
|||
return guard.forbidden();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/users/confirm-email',
|
||||
page: 'pages/Users/OauthBindEmail',
|
||||
},
|
||||
{
|
||||
path: '/users/oauth',
|
||||
page: 'pages/Users/OauthCallback',
|
||||
},
|
||||
{
|
||||
path: '/posts/:qid/timeline',
|
||||
page: 'pages/Timeline',
|
||||
|
@ -324,6 +336,14 @@ const routes: RouteNode[] = [
|
|||
path: 'login',
|
||||
page: 'pages/Admin/Login',
|
||||
},
|
||||
{
|
||||
path: 'installed_plugins',
|
||||
page: 'pages/Admin/Plugins/Installed',
|
||||
},
|
||||
{
|
||||
path: ':slug_name',
|
||||
page: 'pages/Admin/Plugins/Config',
|
||||
},
|
||||
],
|
||||
},
|
||||
// for review
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './question';
|
|||
export * from './settings';
|
||||
export * from './users';
|
||||
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 './revision';
|
||||
export * from './user';
|
||||
export * from './Oauth';
|
||||
|
|
|
@ -249,7 +249,7 @@ export const closeQuestion = (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);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import loginSettingStore from '@/stores/loginSetting';
|
|||
import seoSettingStore from '@/stores/seoSetting';
|
||||
|
||||
import toastStore from './toast';
|
||||
import loggedUserInfoStore from './userInfo';
|
||||
import loggedUserInfoStore from './loggedUserInfoStore';
|
||||
import siteInfoStore from './siteInfo';
|
||||
import interfaceStore from './interface';
|
||||
import brandingStore from './branding';
|
||||
|
|
|
@ -26,12 +26,17 @@ const initUser: UserInfoRes = {
|
|||
status: '',
|
||||
mail_status: 1,
|
||||
language: 'Default',
|
||||
is_admin: false,
|
||||
have_password: true,
|
||||
role_id: 1,
|
||||
};
|
||||
|
||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
user: initUser,
|
||||
update: (params) => {
|
||||
if (typeof params !== 'object' || !params) {
|
||||
return;
|
||||
}
|
||||
if (!params?.language) {
|
||||
params.language = 'Default';
|
||||
}
|
|
@ -233,6 +233,26 @@ function diffText(newText: string, oldText?: string): string {
|
|||
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 {
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
|
@ -248,4 +268,5 @@ export {
|
|||
labelStyle,
|
||||
handleFormError,
|
||||
diffText,
|
||||
base64ToSvg,
|
||||
};
|
||||
|
|
|
@ -11,10 +11,10 @@ import {
|
|||
loginToContinueStore,
|
||||
} from '@/stores';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import Storage from '@/utils/storage';
|
||||
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';
|
||||
|
||||
type TLoginState = {
|
||||
|
@ -98,7 +98,6 @@ export const pullLoggedUser = async (forceRePull = false) => {
|
|||
if (Date.now() - dedupeTimestamp < 1000 * 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
dedupeTimestamp = Date.now();
|
||||
const loggedUserInfo = await getLoggedUserInfo().catch((ex) => {
|
||||
dedupeTimestamp = 0;
|
||||
|
@ -298,6 +297,10 @@ export const tryLoggedAndActivated = () => {
|
|||
return gr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize app configuration
|
||||
*/
|
||||
let appInitialized = false;
|
||||
export const initAppSettingsStore = async () => {
|
||||
const appSettings = await getAppSettings();
|
||||
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 () => {
|
||||
if (appInitialized) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* WARN:
|
||||
* 1. must pre init logged user info for router guard
|
||||
* 2. must pre init app settings for app render
|
||||
*/
|
||||
// TODO: optimize `initAppSettingsStore` by server render
|
||||
if (shouldInitAppFetchData()) {
|
||||
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
||||
await setupAppLanguage();
|
||||
setupAppLanguage();
|
||||
setupAppTimeZone();
|
||||
}
|
||||
appInitialized = true;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ export { floppyNavigation } from './floppyNavigation';
|
|||
export { default as storageExpires } from './storageWithExpires';
|
||||
export { default as SaveDraft } from './saveDraft';
|
||||
|
||||
export * as guard from './guard';
|
||||
export * as localize from './localize';
|
||||
export * from './common';
|
||||
export * from './color';
|
||||
export * as localize from './localize';
|
||||
export * as guard from './guard';
|
||||
|
|
|
@ -9,13 +9,14 @@ import {
|
|||
DEFAULT_LANG,
|
||||
LANG_RESOURCE_STORAGE_KEY,
|
||||
} from '@/common/constants';
|
||||
import { Storage } from '@/utils';
|
||||
import {
|
||||
getAdminLanguageOptions,
|
||||
getLanguageConfig,
|
||||
getLanguageOptions,
|
||||
} from '@/services';
|
||||
|
||||
import Storage from './storage';
|
||||
|
||||
export const loadLanguageOptions = async (forAdmin = false) => {
|
||||
const languageOptions = forAdmin
|
||||
? await getAdminLanguageOptions()
|
||||
|
|
Loading…
Reference in New Issue