mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/1.1.2/ui' into beta.2/1.1.0
This commit is contained in:
commit
d61e0dab9e
|
@ -340,6 +340,7 @@ ui:
|
|||
until_time: "Your account was suspended until {{ time }}."
|
||||
forever: This user was suspended forever.
|
||||
end: You don't meet a community guideline.
|
||||
contact_us: Contact us
|
||||
editor:
|
||||
blockquote:
|
||||
text: Blockquote
|
||||
|
@ -733,6 +734,7 @@ ui:
|
|||
label: Confirm New Password
|
||||
settings:
|
||||
page_title: Settings
|
||||
goto_modify: Go to Modify
|
||||
nav:
|
||||
profile: Profile
|
||||
notification: Notifications
|
||||
|
@ -897,6 +899,10 @@ ui:
|
|||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
pinned: Pinned
|
||||
all: All
|
||||
question: Question
|
||||
answer: Answer
|
||||
comment: Comment
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -1119,12 +1125,16 @@ ui:
|
|||
seo: SEO
|
||||
customize: Customize
|
||||
themes: Themes
|
||||
css-html: CSS/HTML
|
||||
css_html: CSS/HTML
|
||||
login: Login
|
||||
privileges: Privileges
|
||||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
|
@ -1171,6 +1181,7 @@ ui:
|
|||
pending: Pending
|
||||
completed: Completed
|
||||
flagged: Flagged
|
||||
flagged_type: Flagged {{ type }}
|
||||
created: Created
|
||||
action: Action
|
||||
review: Review
|
||||
|
@ -1321,9 +1332,6 @@ ui:
|
|||
label: Timezone
|
||||
msg: Timezone cannot be empty.
|
||||
text: Choose a city in the same timezone as you.
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
smtp:
|
||||
page_title: SMTP
|
||||
from_email:
|
||||
|
@ -1433,12 +1441,22 @@ ui:
|
|||
footer:
|
||||
label: Footer
|
||||
text: This will insert before </html>.
|
||||
sidebar:
|
||||
label: Sidebar
|
||||
text: This will insert in sidebar.
|
||||
login:
|
||||
page_title: Login
|
||||
membership:
|
||||
title: Membership
|
||||
label: Allow new registrations
|
||||
text: Turn off to prevent anyone from creating a new account.
|
||||
email_registration:
|
||||
title: Email registration
|
||||
label: Allow email registration
|
||||
text: Turn off to prevent anyone creating new account through email.
|
||||
allowed_email_domains:
|
||||
title: Allowed email domains
|
||||
text: Email domains that users must register accounts with. One domain per line. Ignored when empty.
|
||||
private:
|
||||
title: Private
|
||||
label: Login required
|
||||
|
@ -1460,7 +1478,30 @@ ui:
|
|||
deactivate: Deactivate
|
||||
activate: Activate
|
||||
settings: Settings
|
||||
|
||||
settings_users:
|
||||
title: Users
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
profile_editable:
|
||||
title: Profile Editable
|
||||
allow_update_display_name:
|
||||
label: Allow users to change their display name
|
||||
allow_update_username:
|
||||
label: Allow users to change their username
|
||||
allow_update_avatar:
|
||||
label: Allow users to change their profile image
|
||||
allow_update_bio:
|
||||
label: Allow users to change their about me
|
||||
allow_update_website:
|
||||
label: Allow users to change their website
|
||||
allow_update_location:
|
||||
label: Allow users to change their location
|
||||
privilege:
|
||||
title: Privileges
|
||||
level:
|
||||
label: Reputation required level
|
||||
text: Choose the reputation required for the privileges
|
||||
|
||||
form:
|
||||
optional: (optional)
|
||||
|
|
|
@ -837,6 +837,10 @@ ui:
|
|||
skip: 略过
|
||||
discard_draft: 丢弃草稿
|
||||
pinned: 已置顶
|
||||
all: 所有
|
||||
question: 问题
|
||||
answer: 回答
|
||||
comment: 评论
|
||||
search:
|
||||
title: 搜索结果
|
||||
keywords: 关键词
|
||||
|
@ -1103,6 +1107,7 @@ ui:
|
|||
pending: 等待处理
|
||||
completed: 已完成
|
||||
flagged: 被举报内容
|
||||
flagged_type: 被举报的{{ type }}
|
||||
created: 创建于
|
||||
action: 操作
|
||||
review: 审查
|
||||
|
|
|
@ -36,6 +36,7 @@ module.exports = {
|
|||
'react/no-unescaped-entities': 'off',
|
||||
'react/require-default-props': 'off',
|
||||
'arrow-body-style': 'off',
|
||||
"global-require": "off",
|
||||
'react/prop-types': 0,
|
||||
'react/no-danger': 'off',
|
||||
'jsx-a11y/no-static-element-interactions': 'off',
|
||||
|
|
|
@ -15,14 +15,13 @@
|
|||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"bootstrap": "^5.2.0",
|
||||
"bootstrap-icons": "1.10.2",
|
||||
"bootstrap-icons": "1.10.4",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.0",
|
||||
"color": "^4.2.3",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -30,6 +29,7 @@
|
|||
"md5": "^2.3.0",
|
||||
"mermaid": "^9.1.7",
|
||||
"next-share": "^0.18.1",
|
||||
"qrcode": "^1.5.1",
|
||||
"qs": "^6.11.0",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.5.0",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,8 +2,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#0033FF" />
|
||||
<meta name="generator" content="Answer - https://github.com/answerdev/answer">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
</head>
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -2,25 +2,18 @@ export const DEFAULT_SITE_NAME = 'Answer';
|
|||
export const DEFAULT_LANG = 'en_US';
|
||||
export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
|
||||
export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
|
||||
export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
|
||||
export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
|
||||
export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
|
||||
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
|
||||
export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
|
||||
export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
|
||||
export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
|
||||
|
||||
export const IGNORE_PATH_LIST = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/change-email',
|
||||
'/users/password-reset',
|
||||
'/users/account-activation',
|
||||
'/users/account-activation/success',
|
||||
'/users/account-activation/failed',
|
||||
'/users/confirm-new-email',
|
||||
];
|
||||
export const USER_AGENT_NAMES = {
|
||||
SegmentFault: 'SegmentFault',
|
||||
WeChat: 'WeChat',
|
||||
WeCom: 'WeCom',
|
||||
DingTalk: 'DingTalk',
|
||||
};
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
|
@ -75,7 +68,8 @@ export const ADMIN_NAV_MENUS = [
|
|||
name: 'themes',
|
||||
},
|
||||
{
|
||||
name: 'css-html',
|
||||
name: 'css_html',
|
||||
path: 'css-html',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -90,6 +84,8 @@ export const ADMIN_NAV_MENUS = [
|
|||
{ name: 'write' },
|
||||
{ name: 'seo' },
|
||||
{ name: 'login' },
|
||||
{ name: 'users', path: 'settings-users' },
|
||||
{ name: 'privileges' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -97,6 +93,7 @@ export const ADMIN_NAV_MENUS = [
|
|||
children: [
|
||||
{
|
||||
name: 'installed_plugins',
|
||||
path: 'installed-plugins',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface FormValue<T = any> {
|
||||
value: T;
|
||||
isInvalid: boolean;
|
||||
|
@ -246,6 +244,7 @@ export type QuestionOrderBy =
|
|||
export interface QueryQuestionsReq extends Paging {
|
||||
order: QuestionOrderBy;
|
||||
tag?: string;
|
||||
in_days?: number;
|
||||
}
|
||||
|
||||
export type AdminQuestionStatus = 'available' | 'closed' | 'deleted';
|
||||
|
@ -312,7 +311,6 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
|
|||
export interface AdminSettingsInterface {
|
||||
language: string;
|
||||
time_zone?: string;
|
||||
default_avatar?: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsSmtp {
|
||||
|
@ -327,6 +325,16 @@ export interface AdminSettingsSmtp {
|
|||
test_email_recipient?: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsUsers {
|
||||
allow_update_avatar: boolean;
|
||||
allow_update_bio: boolean;
|
||||
allow_update_display_name: boolean;
|
||||
allow_update_location: boolean;
|
||||
allow_update_username: boolean;
|
||||
allow_update_website: boolean;
|
||||
default_avatar: string;
|
||||
}
|
||||
|
||||
export interface SiteSettings {
|
||||
branding: AdminSettingBranding;
|
||||
general: AdminSettingsGeneral;
|
||||
|
@ -335,6 +343,7 @@ export interface SiteSettings {
|
|||
custom_css_html: AdminSettingsCustom;
|
||||
theme: AdminSettingsTheme;
|
||||
site_seo: AdminSettingsSeo;
|
||||
site_users: AdminSettingsUsers;
|
||||
version: string;
|
||||
revision: string;
|
||||
}
|
||||
|
@ -385,11 +394,14 @@ export interface AdminSettingsCustom {
|
|||
custom_head: string;
|
||||
custom_header: string;
|
||||
custom_footer: string;
|
||||
custom_sidebar: string;
|
||||
}
|
||||
|
||||
export interface AdminSettingsLogin {
|
||||
allow_new_registrations: boolean;
|
||||
login_required: boolean;
|
||||
allow_email_registrations: boolean;
|
||||
allow_email_domains: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -556,27 +568,6 @@ 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;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const pattern = {
|
||||
emoji: emojiRegex(),
|
||||
email:
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
|
||||
uaWeChat: /micromessenger/i,
|
||||
uaWeCom: /wxwork/i,
|
||||
uaDingTalk: /dingtalk/i,
|
||||
};
|
||||
|
||||
export default pattern;
|
||||
|
|
|
@ -18,12 +18,12 @@ function MenuNode({
|
|||
}) {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
|
||||
const isLeaf = !menu.children.length;
|
||||
const href = isLeaf ? `${path}${menu.name}` : '#';
|
||||
const href = isLeaf ? `${path}${menu.path}` : '#';
|
||||
|
||||
return (
|
||||
<Nav.Item key={menu.name} className="w-100">
|
||||
<Nav.Item key={menu.path} className="w-100">
|
||||
<Nav.Link
|
||||
eventKey={menu.name}
|
||||
eventKey={menu.path}
|
||||
as={isLeaf ? 'a' : 'button'}
|
||||
onClick={(evt) => {
|
||||
callback(evt, menu, href, isLeaf);
|
||||
|
@ -31,7 +31,7 @@ function MenuNode({
|
|||
href={href}
|
||||
className={classNames(
|
||||
'text-nowrap d-flex flex-nowrap align-items-center w-100',
|
||||
{ expanding, 'link-dark': activeKey !== menu.name },
|
||||
{ expanding, 'link-dark': activeKey !== menu.path },
|
||||
)}>
|
||||
<span className="me-auto text-truncate">
|
||||
{menu.displayName ? menu.displayName : t(menu.name)}
|
||||
|
@ -44,7 +44,7 @@ function MenuNode({
|
|||
)}
|
||||
</Nav.Link>
|
||||
{menu.children.length ? (
|
||||
<Accordion.Collapse eventKey={menu.name} className="ms-3">
|
||||
<Accordion.Collapse eventKey={menu.path} className="ms-3">
|
||||
<>
|
||||
{menu.children.map((leaf) => {
|
||||
return (
|
||||
|
@ -53,7 +53,7 @@ function MenuNode({
|
|||
callback={callback}
|
||||
activeKey={activeKey}
|
||||
path={path}
|
||||
key={leaf.name}
|
||||
key={leaf.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -73,17 +73,24 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
const pathMatch = useMatch(`${path}*`);
|
||||
// auto set menu fields
|
||||
menus.forEach((m) => {
|
||||
if (!m.path) {
|
||||
m.path = m.name;
|
||||
}
|
||||
if (!Array.isArray(m.children)) {
|
||||
m.children = [];
|
||||
}
|
||||
m.children.forEach((sm) => {
|
||||
if (!sm.path) {
|
||||
sm.path = sm.name;
|
||||
}
|
||||
if (!Array.isArray(sm.children)) {
|
||||
sm.children = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const splat = pathMatch && pathMatch.params['*'];
|
||||
let activeKey = menus[0].name;
|
||||
let activeKey = menus[0].path;
|
||||
if (splat) {
|
||||
activeKey = splat;
|
||||
}
|
||||
|
@ -92,10 +99,10 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
menus.forEach((li) => {
|
||||
if (li.children.length) {
|
||||
const matchedChild = li.children.find((el) => {
|
||||
return el.name === activeKey;
|
||||
return el.path === activeKey;
|
||||
});
|
||||
if (matchedChild) {
|
||||
openKey = li.name;
|
||||
openKey = li.path;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -111,7 +118,7 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
navigate(href);
|
||||
}
|
||||
} else {
|
||||
setOpenKey(openKey === menu.name ? '' : menu.name);
|
||||
setOpenKey(openKey === menu.path ? '' : menu.path);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
|
@ -127,8 +134,8 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
path={path}
|
||||
callback={menuClick}
|
||||
activeKey={activeKey}
|
||||
expanding={openKey === li.name}
|
||||
key={li.name}
|
||||
expanding={openKey === li.path}
|
||||
key={li.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -9,9 +9,16 @@ interface Props {
|
|||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
acceptType?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ type = 'post', value, onChange, acceptType }) => {
|
||||
const Index: FC<Props> = ({
|
||||
type = 'post',
|
||||
value,
|
||||
onChange,
|
||||
acceptType,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const onUpload = (imgPath: string) => {
|
||||
onChange(imgPath);
|
||||
};
|
||||
|
@ -29,11 +36,15 @@ const Index: FC<Props> = ({ type = 'post', value, onChange, acceptType }) => {
|
|||
type={type}
|
||||
uploadCallback={onUpload}
|
||||
className="mb-0"
|
||||
disabled={readOnly}
|
||||
acceptType={acceptType}>
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
|
||||
<Button variant="outline-secondary" onClick={onRemove}>
|
||||
<Button
|
||||
disabled={readOnly}
|
||||
variant="outline-secondary"
|
||||
onClick={onRemove}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { memo } from 'react';
|
||||
|
||||
import { customizeStore } from '@/stores';
|
||||
|
||||
const Index = () => {
|
||||
const { custom_sidebar } = customizeStore((state) => state);
|
||||
if (!custom_sidebar) return null;
|
||||
return <div dangerouslySetInnerHTML={{ __html: custom_sidebar }} />;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -11,6 +11,9 @@ const Index: FC = () => {
|
|||
let primaryColor;
|
||||
if (theme_config?.[theme]?.primary_color) {
|
||||
primaryColor = Color(theme_config[theme].primary_color);
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', primaryColor.hex());
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -55,9 +58,12 @@ const Index: FC = () => {
|
|||
--bs-pagination-active-border-color: ${primaryColor.hex()};
|
||||
}
|
||||
.form-select:focus,
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 0 0.25rem ${primaryColor.fade(0.75).string()};
|
||||
border-color: ${tintColor(primaryColor, 0.5)};
|
||||
.form-control:focus,
|
||||
.form-control.focus{
|
||||
box-shadow: 0 0 0 0.25rem ${primaryColor
|
||||
.fade(0.75)
|
||||
.string()} !important;
|
||||
border-color: ${tintColor(primaryColor, 0.5)} !important;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: ${primaryColor.hex()};
|
||||
|
@ -80,7 +86,7 @@ const Index: FC = () => {
|
|||
color: ${primaryColor.hex()}!important;
|
||||
}
|
||||
.link-primary:hover, .link-primary:focus {
|
||||
color: ${shadeColor(primaryColor, 0.8).hex()}!important
|
||||
color: ${shadeColor(primaryColor, 0.8).hex()}!important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
|
|
@ -94,7 +94,7 @@ const Editor = ({
|
|||
return;
|
||||
}
|
||||
if (editor.getValue() !== value) {
|
||||
editor.setValue(value);
|
||||
editor.setValue(value || '');
|
||||
}
|
||||
}, [editor, value]);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'react';
|
||||
|
||||
import { markdownToHtml } from '@/services';
|
||||
import ImgViewer from '@/components/ImgViewer';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
|
@ -48,11 +49,13 @@ const Index = ({ value }, ref) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<ImgViewer>
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</ImgViewer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ const Index = () => {
|
|||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const cc = `${fullYear} ${siteName}`;
|
||||
return (
|
||||
<footer className="bg-light py-3">
|
||||
<Container>
|
||||
<footer className="bg-light">
|
||||
<Container className="py-3">
|
||||
<p className="text-center mb-0 fs-14 text-secondary">
|
||||
<Trans i18nKey="footer.build_on" values={{ cc }}>
|
||||
Built on
|
||||
|
|
|
@ -6,6 +6,7 @@ import { NavLink, useNavigate } from 'react-router-dom';
|
|||
import type * as Type from '@/common/interface';
|
||||
import { Avatar, Icon } from '@/components';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { userCenterStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
redDot: Type.NotificationStatus | undefined;
|
||||
|
@ -15,13 +16,14 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
const { t } = useTranslation();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const navigate = useNavigate();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const handleLinkClick = (evt) => {
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
const { href } = evt.currentTarget;
|
||||
const { pathname } = new URL(href);
|
||||
navigate(pathname);
|
||||
const href = evt.currentTarget.getAttribute('href');
|
||||
navigate(href);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
@ -90,6 +92,44 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
{/* Dropdown for user center agent info */}
|
||||
{ucAgent?.enabled &&
|
||||
(ucAgent?.agent_info?.url ||
|
||||
ucAgent?.agent_info?.control_center?.length) ? (
|
||||
<Dropdown align="end">
|
||||
<Dropdown.Toggle
|
||||
variant="success"
|
||||
id="dropdown-uca"
|
||||
as="span"
|
||||
className="no-toggle">
|
||||
<Nav>
|
||||
<Icon
|
||||
name="grid-3x3-gap-fill"
|
||||
className="nav-link pointer p-0 fs-4 ms-3"
|
||||
/>
|
||||
</Nav>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
{ucAgent.agent_info.url ? (
|
||||
<Dropdown.Item href={ucAgent.agent_info.url}>
|
||||
{ucAgent.agent_info.name}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{ucAgent.agent_info.url &&
|
||||
ucAgent.agent_info.control_center?.length ? (
|
||||
<Dropdown.Divider />
|
||||
) : null}
|
||||
{ucAgent.agent_info.control_center?.map((ctrl) => {
|
||||
return (
|
||||
<Dropdown.Item key={ctrl.name} href={ctrl.url}>
|
||||
{ctrl.label}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
background: linear-gradient(180deg, rgb(var(--bs-primary-rgb)) 0%, rgba(var(--bs-primary-rgb), 0.95) 100%);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 0.125rem 0.25rem rgb(0 0 0 / 8%);
|
||||
.logo {
|
||||
max-height: 1.75rem;
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
|
@ -57,9 +57,6 @@
|
|||
|
||||
@media (max-width: 992.9px) {
|
||||
#header {
|
||||
.logo {
|
||||
max-width: 93px;
|
||||
}
|
||||
.nav-grow {
|
||||
flex-grow: 1!important;
|
||||
}
|
||||
|
@ -75,12 +72,3 @@
|
|||
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
#header {
|
||||
.logo {
|
||||
max-width: 93px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { floppyNavigation, userCenter } from '@/utils';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
|
@ -29,6 +29,7 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -46,6 +47,9 @@ const Header: FC = () => {
|
|||
const brandingInfo = brandingStore((state) => state.branding);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const { data: redDot } = useQueryNotificationStatus();
|
||||
/**
|
||||
* Automatically append `tag` information when creating a question
|
||||
*/
|
||||
const tagMatch = useMatch('/tags/:slugName');
|
||||
let askUrl = '/questions/ask';
|
||||
if (tagMatch && tagMatch.params.slugName) {
|
||||
|
@ -70,15 +74,15 @@ const Header: FC = () => {
|
|||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
if (location.pathname === '/users/login') {
|
||||
if (location.pathname === RouteAlias.login) {
|
||||
evt.preventDefault();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin((loginPath) => {
|
||||
navigate(loginPath, { replace: true });
|
||||
floppyNavigation.navigateToLogin({
|
||||
handler: navigate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -119,17 +123,17 @@ const Header: FC = () => {
|
|||
/>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap">
|
||||
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3">
|
||||
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3 p-0">
|
||||
{brandingInfo.logo ? (
|
||||
<>
|
||||
<img
|
||||
className="d-none d-lg-block logo rounded-1 me-0"
|
||||
className="d-none d-lg-block logo me-0"
|
||||
src={brandingInfo.logo}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<img
|
||||
className="lg-none logo rounded-1 me-0"
|
||||
className="lg-none logo me-0"
|
||||
src={brandingInfo.mobile_logo || brandingInfo.logo}
|
||||
alt=""
|
||||
/>
|
||||
|
@ -152,7 +156,7 @@ const Header: FC = () => {
|
|||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -160,7 +164,7 @@ const Header: FC = () => {
|
|||
variant={
|
||||
navbarStyle === 'theme-colored' ? 'light' : 'primary'
|
||||
}
|
||||
href="/users/register">
|
||||
href={userCenter.getSignUpUrl()}>
|
||||
{t('btns.signup')}
|
||||
</Button>
|
||||
)}
|
||||
|
@ -240,7 +244,7 @@ const Header: FC = () => {
|
|||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href="/users/login">
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
|
@ -248,7 +252,7 @@ const Header: FC = () => {
|
|||
variant={
|
||||
navbarStyle === 'theme-colored' ? 'light' : 'primary'
|
||||
}
|
||||
href="/users/register">
|
||||
href={userCenter.getSignUpUrl()}>
|
||||
{t('btns.signup')}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Card, ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -9,23 +9,17 @@ import { useHotQuestions } from '@/services';
|
|||
|
||||
const HotQuestions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const [questions, setQuestions] = useState<any>([]);
|
||||
const { data: questionRes } = useHotQuestions();
|
||||
|
||||
useEffect(() => {
|
||||
const questionResp = questionRes?.list;
|
||||
if (Array.isArray(questionResp)) {
|
||||
setQuestions(questionResp);
|
||||
}
|
||||
}, [questionRes]);
|
||||
|
||||
if (!questionRes?.list?.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header className="text-nowrap text-capitalize">
|
||||
{t('hot_questions')}
|
||||
</Card.Header>
|
||||
<ListGroup variant="flush">
|
||||
{questions.map((li) => {
|
||||
{questionRes?.list?.map((li) => {
|
||||
return (
|
||||
<ListGroupItem
|
||||
key={li.id}
|
||||
|
|
|
@ -4,7 +4,12 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index = ({ httpCode = '', errMsg = '' }) => {
|
||||
const Index = ({
|
||||
httpCode = '',
|
||||
title = '',
|
||||
errMsg = '',
|
||||
showErrorCode = true,
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
|
||||
useEffect(() => {
|
||||
// auto height of container
|
||||
|
@ -31,7 +36,10 @@ const Index = ({ httpCode = '', errMsg = '' }) => {
|
|||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
||||
{showErrorCode && (
|
||||
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
||||
)}
|
||||
{title && <h4 className="text-center">{title}</h4>}
|
||||
<div className="text-center mb-3 fs-5">
|
||||
{errMsg || t(`desc_${httpCode}`)}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { base64ToSvg } from '@/utils';
|
||||
|
||||
interface IProps {
|
||||
base64: string | undefined;
|
||||
}
|
||||
const Icon: FC<IProps> = ({ base64 = '' }) => {
|
||||
return base64 ? (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base64ToSvg(base64),
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default Icon;
|
|
@ -0,0 +1,7 @@
|
|||
.img-viewer .cursor-zoom-out {
|
||||
cursor: zoom-out !important;
|
||||
}
|
||||
|
||||
.img-viewer img:not(a img, img.broken) {
|
||||
cursor: zoom-in;
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import { useLayoutEffect, useState, MouseEvent, useEffect } from 'react';
|
||||
import { FC, MouseEvent, ReactNode, useEffect, useState } from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const div = document.createElement('div');
|
||||
const root = ReactDOM.createRoot(div);
|
||||
|
||||
const useImgViewer = () => {
|
||||
const location = useLocation();
|
||||
const Index: FC<{
|
||||
children: ReactNode;
|
||||
className?: classnames.Argument;
|
||||
}> = ({ children, className }) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [imgSrc, setImgSrc] = useState('');
|
||||
const onClose = () => {
|
||||
|
@ -47,8 +46,18 @@ const useImgViewer = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
root.render(
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
onClose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
className={classnames('img-viewer', className)}
|
||||
onClick={checkClickForImgView}>
|
||||
{children}
|
||||
<Modal
|
||||
show={visible}
|
||||
fullscreen
|
||||
|
@ -56,23 +65,21 @@ const useImgViewer = () => {
|
|||
scrollable
|
||||
contentClassName="bg-transparent"
|
||||
onHide={onClose}>
|
||||
<Modal.Body onClick={onClose} className="p-0 d-flex">
|
||||
<Modal.Body onClick={onClose} className="img-viewer p-0 d-flex">
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<img
|
||||
className="cursor-zoom-out img-fluid m-auto"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
src={imgSrc}
|
||||
alt={imgSrc}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>,
|
||||
);
|
||||
});
|
||||
useEffect(() => {
|
||||
onClose();
|
||||
}, [location]);
|
||||
return {
|
||||
onClose,
|
||||
checkClickForImgView,
|
||||
};
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default useImgViewer;
|
||||
export default Index;
|
|
@ -0,0 +1,105 @@
|
|||
import React, { FC, useLayoutEffect, useState } from 'react';
|
||||
import { Button, ButtonProps, Spinner } from 'react-bootstrap';
|
||||
|
||||
import { request } from '@/utils';
|
||||
import type { UIAction, FormKit } from '../types';
|
||||
import { useToast } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
fieldName: string;
|
||||
text: string;
|
||||
action: UIAction | undefined;
|
||||
formKit: FormKit;
|
||||
readOnly: boolean;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
fieldName,
|
||||
action,
|
||||
formKit,
|
||||
text = '',
|
||||
readOnly = false,
|
||||
variant = 'primary',
|
||||
size,
|
||||
}) => {
|
||||
const Toast = useToast();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const handleToast = (msg, type: 'success' | 'danger' = 'success') => {
|
||||
const tm = action?.on_complete?.toast_return_message;
|
||||
if (tm === false || !msg) {
|
||||
return;
|
||||
}
|
||||
Toast.onShow({
|
||||
msg,
|
||||
variant: type,
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleCallback = (resp) => {
|
||||
const callback = action?.on_complete;
|
||||
if (callback?.refresh_form_config) {
|
||||
formKit.refreshConfig();
|
||||
}
|
||||
};
|
||||
const handleAction = () => {
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
request
|
||||
.request({
|
||||
method: action.method,
|
||||
url: action.url,
|
||||
timeout: 0,
|
||||
})
|
||||
.then((resp) => {
|
||||
if ('message' in resp) {
|
||||
handleToast(resp.message, 'success');
|
||||
}
|
||||
handleCallback(resp);
|
||||
})
|
||||
.catch((ex) => {
|
||||
if (ex && 'msg' in ex) {
|
||||
handleToast(ex.msg, 'danger');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (action?.loading?.state === 'pending') {
|
||||
setLoading(true);
|
||||
}
|
||||
}, []);
|
||||
const loadingText = action?.loading?.text || text;
|
||||
const disabled = isLoading || readOnly;
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
name={fieldName}
|
||||
onClick={handleAction}
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
variant={variant}>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
className="align-middle me-2"
|
||||
animation="border"
|
||||
size="sm"
|
||||
variant={variant}
|
||||
/>
|
||||
{loadingText}
|
||||
</>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,67 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form, Stack } from 'react-bootstrap';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
type: 'radio' | 'checkbox';
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
enumValues: (string | boolean | number)[];
|
||||
enumNames: string[];
|
||||
formData: Type.FormDataType;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
type = 'radio',
|
||||
fieldName,
|
||||
onChange,
|
||||
enumValues,
|
||||
enumNames,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleCheck = (
|
||||
evt: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number,
|
||||
) => {
|
||||
const { name, checked } = evt.currentTarget;
|
||||
const freshVal = checked ? enumValues?.[index] : '';
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value: freshVal,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Stack direction="horizontal">
|
||||
{enumValues?.map((item, index) => {
|
||||
return (
|
||||
<Form.Check
|
||||
key={String(item)}
|
||||
inline
|
||||
type={type}
|
||||
name={fieldName}
|
||||
id={`form-${String(item)}`}
|
||||
label={enumNames?.[index]}
|
||||
checked={(fieldObject?.value || '') === item}
|
||||
feedback={fieldObject?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
disabled={readOnly}
|
||||
onChange={(evt) => handleCheck(evt, index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,52 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
type: string | undefined;
|
||||
placeholder: string | undefined;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
readOnly: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
type = 'text',
|
||||
placeholder = '',
|
||||
fieldName,
|
||||
onChange,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = evt.currentTarget;
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
name={fieldName}
|
||||
placeholder={placeholder}
|
||||
type={type}
|
||||
value={fieldObject?.value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
style={type === 'color' ? { width: '6rem' } : {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,11 @@
|
|||
import { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ title }) => {
|
||||
return <Form.Label>{title}</Form.Label>;
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,58 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
desc: string | undefined;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
enumValues: (string | boolean | number)[];
|
||||
enumNames: string[];
|
||||
formData: Type.FormDataType;
|
||||
readOnly: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
desc,
|
||||
fieldName,
|
||||
onChange,
|
||||
enumValues,
|
||||
enumNames,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = evt.currentTarget;
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Form.Select
|
||||
aria-label={desc}
|
||||
name={fieldName}
|
||||
value={fieldObject?.value || ''}
|
||||
onChange={handleChange}
|
||||
disabled={readOnly}
|
||||
isInvalid={fieldObject?.isInvalid}>
|
||||
{enumValues?.map((item, index) => {
|
||||
return (
|
||||
<option value={String(item)} key={String(item)}>
|
||||
{enumNames?.[index]}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Form.Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,53 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
label: string | undefined;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
title,
|
||||
fieldName,
|
||||
onChange,
|
||||
label,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, checked } = evt.currentTarget;
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value: checked,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Form.Check
|
||||
id={`switch-${title}`}
|
||||
name={fieldName}
|
||||
type="switch"
|
||||
label={label}
|
||||
checked={fieldObject?.value || ''}
|
||||
feedback={fieldObject?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
isInvalid={fieldObject.isInvalid}
|
||||
disabled={readOnly}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,57 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
placeholder: string | undefined;
|
||||
rows: number | undefined;
|
||||
className: classnames.Argument;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
readOnly: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
placeholder = '',
|
||||
rows = 3,
|
||||
className,
|
||||
fieldName,
|
||||
onChange,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = evt.currentTarget;
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
name={fieldName}
|
||||
placeholder={placeholder}
|
||||
value={fieldObject?.value || ''}
|
||||
onChange={handleChange}
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
rows={rows}
|
||||
disabled={readOnly}
|
||||
className={classnames(className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,44 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import TimeZonePicker from '@/components/TimeZonePicker';
|
||||
|
||||
interface Props {
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
fieldName,
|
||||
onChange,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = evt.currentTarget;
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<TimeZonePicker
|
||||
value={fieldObject?.value || ''}
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
name={fieldName}
|
||||
disabled={readOnly}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,54 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
import BrandUpload from '@/components/BrandUpload';
|
||||
|
||||
interface Props {
|
||||
type: Type.UploadType | undefined;
|
||||
acceptType: string | undefined;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
type = 'avatar',
|
||||
acceptType = '',
|
||||
fieldName,
|
||||
onChange,
|
||||
formData,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (name: string, value: string) => {
|
||||
const state = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<BrandUpload
|
||||
type={type}
|
||||
acceptType={acceptType}
|
||||
value={fieldObject?.value}
|
||||
readOnly={readOnly}
|
||||
onChange={(value) => handleChange(fieldName, value)}
|
||||
/>
|
||||
<Form.Control
|
||||
name={fieldName}
|
||||
className="d-none"
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,21 @@
|
|||
import Legend from './Legend';
|
||||
import Select from './Select';
|
||||
import Check from './Check';
|
||||
import Switch from './Switch';
|
||||
import Timezone from './Timezone';
|
||||
import Upload from './Upload';
|
||||
import Textarea from './Textarea';
|
||||
import Input from './Input';
|
||||
import Button from './Button';
|
||||
|
||||
export {
|
||||
Legend,
|
||||
Select,
|
||||
Check,
|
||||
Switch,
|
||||
Timezone,
|
||||
Upload,
|
||||
Textarea,
|
||||
Input,
|
||||
Button,
|
||||
};
|
|
@ -1,113 +1,37 @@
|
|||
import {
|
||||
import React, {
|
||||
ForwardRefRenderFunction,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { Form, Button, Stack } from 'react-bootstrap';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import BrandUpload from '../BrandUpload';
|
||||
import TimeZonePicker from '../TimeZonePicker';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
default?: string | boolean | number;
|
||||
};
|
||||
};
|
||||
}
|
||||
import type { JSONSchema, UISchema, BaseUIOptions, FormKit } from './types';
|
||||
import {
|
||||
Legend,
|
||||
Select,
|
||||
Check,
|
||||
Switch,
|
||||
Timezone,
|
||||
Upload,
|
||||
Textarea,
|
||||
Input,
|
||||
Button as SfButton,
|
||||
} from './components';
|
||||
|
||||
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 {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
export * from './types';
|
||||
|
||||
interface IProps {
|
||||
schema: JSONSchema;
|
||||
schema: JSONSchema | null;
|
||||
formData: Type.FormDataType | null;
|
||||
uiSchema?: UISchema;
|
||||
formData?: Type.FormDataType;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
|
@ -117,6 +41,17 @@ interface IRef {
|
|||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.
|
||||
* - Normalize and document `formData[key].hidden && 'd-none'`
|
||||
* - Normalize and document `hiddenSubmit`
|
||||
* - Improving field hints for `formData`
|
||||
* - Optimise form data updates
|
||||
* * Automatic field type conversion
|
||||
* * Dynamic field generation
|
||||
*/
|
||||
|
||||
/**
|
||||
* json schema form
|
||||
* @param schema json schema
|
||||
|
@ -129,7 +64,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
{
|
||||
schema,
|
||||
uiSchema = {},
|
||||
formData = {},
|
||||
refreshConfig,
|
||||
formData,
|
||||
onChange,
|
||||
onSubmit,
|
||||
hiddenSubmit = false,
|
||||
|
@ -139,16 +75,13 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'form',
|
||||
});
|
||||
|
||||
const { required = [], properties } = schema;
|
||||
|
||||
const { required = [], properties = {} } = schema || {};
|
||||
// check required field
|
||||
const excludes = required.filter((key) => !properties[key]);
|
||||
|
||||
if (excludes.length > 0) {
|
||||
console.error(t('not_found_props', { key: excludes.join(', ') }));
|
||||
}
|
||||
|
||||
formData ||= {};
|
||||
const keys = Object.keys(properties);
|
||||
/**
|
||||
* Prevent components such as `select` from having default values,
|
||||
|
@ -156,14 +89,14 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
*/
|
||||
const setDefaultValueAsDomBehaviour = () => {
|
||||
keys.forEach((k) => {
|
||||
const fieldVal = formData[k]?.value;
|
||||
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] = {
|
||||
formData![k] = {
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
value: metaProp.enum?.[0],
|
||||
|
@ -175,43 +108,22 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
setDefaultValueAsDomBehaviour();
|
||||
}, [formData]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const data = {
|
||||
...formData,
|
||||
[name]: { ...formData[name], value, isInvalid: false },
|
||||
};
|
||||
if (onChange instanceof Function) {
|
||||
onChange(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const data = {
|
||||
...formData,
|
||||
[name]: { ...formData[name], value, isInvalid: false },
|
||||
};
|
||||
if (onChange instanceof Function) {
|
||||
onChange(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, checked } = e.target;
|
||||
const data = {
|
||||
...formData,
|
||||
[name]: { ...formData[name], value: checked, isInvalid: false },
|
||||
};
|
||||
if (onChange instanceof Function) {
|
||||
onChange(data);
|
||||
}
|
||||
const formKitWithContext: FormKit = {
|
||||
refreshConfig() {
|
||||
if (typeof refreshConfig === 'function') {
|
||||
refreshConfig();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Form validation
|
||||
* - Currently only dynamic forms are in use, the business form validation has been handed over to the server
|
||||
*/
|
||||
const requiredValidator = () => {
|
||||
const errors: string[] = [];
|
||||
required.forEach((key) => {
|
||||
if (!formData[key] || !formData[key].value) {
|
||||
if (!formData![key] || !formData![key].value) {
|
||||
errors.push(key);
|
||||
}
|
||||
});
|
||||
|
@ -227,7 +139,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
keys.forEach((key) => {
|
||||
const { validator } = uiSchema[key]?.['ui:options'] || {};
|
||||
if (validator instanceof Function) {
|
||||
const value = formData[key]?.value;
|
||||
const value = formData![key]?.value;
|
||||
promises.push({
|
||||
key,
|
||||
promise: validator(value, formData),
|
||||
|
@ -265,14 +177,14 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
if (errors.length > 0) {
|
||||
formData = errors.reduce((acc, cur) => {
|
||||
acc[cur] = {
|
||||
...formData[cur],
|
||||
...formData![cur],
|
||||
isInvalid: true,
|
||||
errorMsg:
|
||||
uiSchema[cur]?.['ui:options']?.empty ||
|
||||
`${schema.properties[cur]?.title} ${t('empty')}`,
|
||||
`${properties[cur]?.title} ${t('empty')}`,
|
||||
};
|
||||
return acc;
|
||||
}, formData);
|
||||
}, formData || {});
|
||||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
|
@ -282,13 +194,12 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
if (syncErrors.length > 0) {
|
||||
formData = syncErrors.reduce((acc, cur) => {
|
||||
acc[cur.key] = {
|
||||
...formData[cur.key],
|
||||
...formData![cur.key],
|
||||
isInvalid: true,
|
||||
errorMsg:
|
||||
cur.msg || `${schema.properties[cur.key].title} ${t('invalid')}`,
|
||||
errorMsg: cur.msg || `${properties[cur.key].title} ${t('invalid')}`,
|
||||
};
|
||||
return acc;
|
||||
}, formData);
|
||||
}, formData || {});
|
||||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
|
@ -304,259 +215,160 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
return;
|
||||
}
|
||||
|
||||
Object.keys(formData).forEach((key) => {
|
||||
formData[key].isInvalid = false;
|
||||
formData[key].errorMsg = '';
|
||||
Object.keys(formData!).forEach((key) => {
|
||||
formData![key].isInvalid = false;
|
||||
formData![key].errorMsg = '';
|
||||
});
|
||||
if (onChange instanceof Function) {
|
||||
onChange(formData);
|
||||
onChange(formData!);
|
||||
}
|
||||
if (onSubmit instanceof Function) {
|
||||
onSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (name: string, value: string) => {
|
||||
const data = { ...formData, [name]: { ...formData[name], value } };
|
||||
if (onChange instanceof Function) {
|
||||
onChange(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputCheck = (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
index: number,
|
||||
) => {
|
||||
const { name, checked } = e.currentTarget;
|
||||
const freshVal = checked ? schema.properties[name]?.enum?.[index] : '';
|
||||
const data = {
|
||||
...formData,
|
||||
[name]: {
|
||||
...formData[name],
|
||||
value: freshVal,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (onChange instanceof Function) {
|
||||
onChange(data);
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
validator,
|
||||
}));
|
||||
if (!formData || !schema || isEmpty(schema.properties)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{keys.map((key) => {
|
||||
const { title, description } = properties[key];
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
enum: enumValues = [],
|
||||
enumNames = [],
|
||||
} = properties[key];
|
||||
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||
uiSchema[key] || {};
|
||||
if (widget === 'select') {
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
controlId={key}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<Form.Select
|
||||
aria-label={description}
|
||||
name={key}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleSelectChange}
|
||||
isInvalid={formData[key].isInvalid}>
|
||||
{properties[key].enum?.map((item, index) => {
|
||||
return (
|
||||
<option value={String(item)} key={String(item)}>
|
||||
{properties[key].enumNames?.[index]}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Form.Select>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
uiSchema?.[key] || {};
|
||||
formData ||= {};
|
||||
const fieldState = formData[key];
|
||||
const uiSimplify = widget === 'legend' || uiOpt?.simplify;
|
||||
let groupClassName: BaseUIOptions['fieldClassName'] = uiOpt?.simplify
|
||||
? 'mb-2'
|
||||
: 'mb-3';
|
||||
if (widget === 'legend') {
|
||||
groupClassName = 'mb-0';
|
||||
}
|
||||
|
||||
if (widget === 'checkbox' || widget === 'radio') {
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
||||
controlId={key}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<Stack direction="horizontal">
|
||||
{properties[key].enum?.map((item, index) => {
|
||||
return (
|
||||
<Form.Check
|
||||
key={String(item)}
|
||||
inline
|
||||
required
|
||||
type={widget}
|
||||
name={key}
|
||||
id={`form-${String(item)}`}
|
||||
label={properties[key].enumNames?.[index]}
|
||||
checked={(formData[key]?.value || '') === item}
|
||||
feedback={formData[key]?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
onChange={(e) => handleInputCheck(e, index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
if (uiOpt?.fieldClassName) {
|
||||
groupClassName = uiOpt.fieldClassName;
|
||||
}
|
||||
|
||||
if (widget === 'switch') {
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
||||
controlId={key}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<Form.Check
|
||||
required
|
||||
id={`switch-${title}`}
|
||||
name={key}
|
||||
type="switch"
|
||||
label={(uiOpt as SwitchOptions)?.label}
|
||||
checked={formData[key]?.value || ''}
|
||||
feedback={formData[key]?.errorMsg}
|
||||
feedbackType="invalid"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
onChange={handleSwitchChange}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
if (widget === 'timezone') {
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
||||
controlId={key}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<TimeZonePicker
|
||||
value={formData[key]?.value || ''}
|
||||
name={key}
|
||||
onChange={handleSelectChange}
|
||||
/>
|
||||
<Form.Control
|
||||
name={key}
|
||||
className="d-none"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (widget === 'upload') {
|
||||
const options: UploadOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}
|
||||
controlId={key}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<BrandUpload
|
||||
type={options.imageType || 'avatar'}
|
||||
acceptType={options.acceptType || ''}
|
||||
value={formData[key]?.value}
|
||||
onChange={(value) => handleUploadChange(key, value)}
|
||||
/>
|
||||
<Form.Control
|
||||
name={key}
|
||||
className="d-none"
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (widget === 'textarea') {
|
||||
const options: TextareaOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
|
||||
return (
|
||||
<Form.Group
|
||||
controlId={`form-${key}`}
|
||||
key={key}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
name={key}
|
||||
placeholder={options?.placeholder || ''}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleInputChange}
|
||||
isInvalid={formData[key].isInvalid}
|
||||
rows={options?.rows || 3}
|
||||
className={classnames(options.className)}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
|
||||
{description && (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
const options: InputOptions = uiSchema[key]?.['ui:options'] || {};
|
||||
|
||||
const readOnly = uiOpt?.readOnly || false;
|
||||
return (
|
||||
<Form.Group
|
||||
key={title}
|
||||
controlId={key}
|
||||
key={key}
|
||||
className={classnames('mb-3', formData[key].hidden && 'd-none')}>
|
||||
<Form.Label>{title}</Form.Label>
|
||||
<Form.Control
|
||||
name={key}
|
||||
placeholder={options?.placeholder || ''}
|
||||
type={options?.inputType || 'text'}
|
||||
value={formData[key]?.value || ''}
|
||||
onChange={handleInputChange}
|
||||
style={options?.inputType === 'color' ? { width: '6rem' } : {}}
|
||||
isInvalid={formData[key].isInvalid}
|
||||
/>
|
||||
className={classnames(
|
||||
groupClassName,
|
||||
formData[key].hidden ? 'd-none' : null,
|
||||
)}>
|
||||
{/* Uniform processing `label` */}
|
||||
{title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
|
||||
{/* Handling of individual specific controls */}
|
||||
{widget === 'legend' ? <Legend title={title} /> : null}
|
||||
{widget === 'select' ? (
|
||||
<Select
|
||||
desc={description}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
enumValues={enumValues}
|
||||
enumNames={enumNames}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'radio' || widget === 'checkbox' ? (
|
||||
<Check
|
||||
type={widget}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
enumValues={enumValues}
|
||||
enumNames={enumNames}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'switch' ? (
|
||||
<Switch
|
||||
title={title}
|
||||
label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'timezone' ? (
|
||||
<Timezone
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'upload' ? (
|
||||
<Upload
|
||||
type={
|
||||
uiOpt && 'imageType' in uiOpt ? uiOpt.imageType : undefined
|
||||
}
|
||||
acceptType={
|
||||
uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''
|
||||
}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'textarea' ? (
|
||||
<Textarea
|
||||
placeholder={
|
||||
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
|
||||
}
|
||||
rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}
|
||||
className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'input' ? (
|
||||
<Input
|
||||
type={uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'}
|
||||
placeholder={
|
||||
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
|
||||
}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
) : null}
|
||||
{widget === 'button' ? (
|
||||
<SfButton
|
||||
fieldName={key}
|
||||
text={uiOpt && 'text' in uiOpt ? uiOpt.text : ''}
|
||||
action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}
|
||||
formKit={formKitWithContext}
|
||||
readOnly={readOnly}
|
||||
variant={
|
||||
uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined
|
||||
}
|
||||
size={uiOpt && 'size' in uiOpt ? uiOpt.size : undefined}
|
||||
/>
|
||||
) : null}
|
||||
{/* Unified handling of `Feedback` and `Text` */}
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData[key]?.errorMsg}
|
||||
{fieldState?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
|
||||
{description && (
|
||||
{description ? (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
)}
|
||||
) : null}
|
||||
</Form.Group>
|
||||
);
|
||||
})}
|
||||
|
@ -570,9 +382,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
};
|
||||
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||
const formData: Type.FormDataType = {};
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
const prop = schema.properties[key];
|
||||
const props: JSONSchema['properties'] = schema?.properties || {};
|
||||
Object.keys(props).forEach((key) => {
|
||||
const prop = props[key];
|
||||
const defaultVal = prop?.default;
|
||||
|
||||
formData[key] = {
|
||||
value: defaultVal,
|
||||
isInvalid: false,
|
||||
|
@ -582,4 +396,27 @@ export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
|||
return formData;
|
||||
};
|
||||
|
||||
export const mergeFormData = (
|
||||
target: Type.FormDataType | null,
|
||||
origin: Type.FormDataType | null,
|
||||
) => {
|
||||
if (!target) {
|
||||
return origin;
|
||||
}
|
||||
if (!origin) {
|
||||
return target;
|
||||
}
|
||||
Object.keys(target).forEach((k) => {
|
||||
const oi = origin[k];
|
||||
if (oi && oi.value !== undefined) {
|
||||
target[k] = {
|
||||
value: oi.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
return target;
|
||||
};
|
||||
|
||||
export default forwardRef(SchemaForm);
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import { ButtonProps } from 'react-bootstrap';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
default?: string | boolean | number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
// Will be appended to the className of the form component itself
|
||||
className?: classnames.Argument;
|
||||
// The className that will be attached to a form field container
|
||||
fieldClassName?: classnames.Argument;
|
||||
// Make a form component render into simplified mode
|
||||
readOnly?: boolean;
|
||||
simplify?: boolean;
|
||||
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 interface ButtonOptions extends BaseUIOptions {
|
||||
text: string;
|
||||
icon?: string;
|
||||
action?: UIAction;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
|
||||
export type UIOptions =
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions
|
||||
| ButtonOptions;
|
||||
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch'
|
||||
| 'legend'
|
||||
| 'button';
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A few notes on button control:
|
||||
* - Mainly used to send a request and notify the result of the request, and to update the data as required
|
||||
* - A scenario where a message notification is displayed directly after a click without sending a request, implementing a dedicated control
|
||||
* - Scenarios where the page jumps directly after a click without sending a request, implementing a dedicated control
|
||||
*
|
||||
* @field url : Target address for sending requests
|
||||
* @field method : Method for sending requests, default `get`
|
||||
* @field callback: Button event handler function that will fully take over the button events when this field is configured
|
||||
* *** Incomplete, DO NOT USE ***
|
||||
* @field loading: Set button loading information
|
||||
* @field on_complete: What needs to be done when the `Action` completes
|
||||
* @field on_complete.toast_return_message: Does toast show the returned message
|
||||
* @field on_complete.refresh_form_config: Whether to refresh the form configuration (configuration only, no data included)
|
||||
*/
|
||||
export interface UIAction {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'delete';
|
||||
loading?: {
|
||||
text: string;
|
||||
state?: 'none' | 'pending' | 'completed';
|
||||
};
|
||||
on_complete?: {
|
||||
toast_return_message?: boolean;
|
||||
refresh_form_config?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Form tools
|
||||
* - Used to get or set the configuration of forms and form items, the value of a form item
|
||||
* * @method refreshConfig(): void
|
||||
*/
|
||||
|
||||
export interface FormKit {
|
||||
refreshConfig(): void;
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { uploadImage } from '@/services';
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
interface IProps {
|
||||
type: Type.UploadType;
|
||||
className?: string;
|
||||
className?: classnames.Argument;
|
||||
children?: React.ReactNode;
|
||||
acceptType?: string;
|
||||
disabled?: boolean;
|
||||
uploadCallback: (img: string) => void;
|
||||
}
|
||||
|
||||
|
@ -18,6 +21,7 @@ const Index: React.FC<IProps> = ({
|
|||
children,
|
||||
acceptType = '',
|
||||
className,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [status, setStatus] = useState(false);
|
||||
|
@ -47,11 +51,15 @@ const Index: React.FC<IProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<label className={`btn btn-outline-secondary uploadBtn ${className}`}>
|
||||
<label
|
||||
className={classnames('btn btn-outline-secondary', className, {
|
||||
disabled: !!disabled,
|
||||
})}>
|
||||
{children || (status ? t('upload_img.loading') : t('upload_img.name'))}
|
||||
<input
|
||||
type="file"
|
||||
className="d-none"
|
||||
disabled={disabled}
|
||||
accept={`image/jpeg,image/jpg,image/png,image/webp${acceptType}`}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
|
|
@ -3,6 +3,7 @@ import Editor, { EditorRef, htmlRender } from './Editor';
|
|||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Icon from './Icon';
|
||||
import SvgIcon from './Icon/svg';
|
||||
import Modal from './Modal';
|
||||
import TagSelector from './TagSelector';
|
||||
import Unactivate from './Unactivate';
|
||||
|
@ -37,12 +38,15 @@ import Counts from './Counts';
|
|||
import QuestionList from './QuestionList';
|
||||
import HotQuestions from './HotQuestions';
|
||||
import HttpErrorContent from './HttpErrorContent';
|
||||
import CustomSidebar from './CustomSidebar';
|
||||
import ImgViewer from './ImgViewer';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
Header,
|
||||
Footer,
|
||||
Icon,
|
||||
SvgIcon,
|
||||
Modal,
|
||||
Unactivate,
|
||||
UploadImg,
|
||||
|
@ -80,5 +84,7 @@ export {
|
|||
QuestionList,
|
||||
HotQuestions,
|
||||
HttpErrorContent,
|
||||
CustomSidebar,
|
||||
ImgViewer,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -10,7 +10,6 @@ import useChangePasswordModal from './useChangePasswordModal';
|
|||
import usePageTags from './usePageTags';
|
||||
import useLoginRedirect from './useLoginRedirect';
|
||||
import usePromptWithUnload from './usePrompt';
|
||||
import useImgViewer from './useImgViewer';
|
||||
|
||||
export {
|
||||
useTagModal,
|
||||
|
@ -25,5 +24,4 @@ export {
|
|||
usePageTags,
|
||||
useLoginRedirect,
|
||||
usePromptWithUnload,
|
||||
useImgViewer,
|
||||
};
|
||||
|
|
|
@ -11,8 +11,11 @@ const Index = () => {
|
|||
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 });
|
||||
floppyNavigation.navigate(redirect, {
|
||||
handler: navigate,
|
||||
options: {
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -102,6 +102,8 @@ a {
|
|||
}
|
||||
#root > footer {
|
||||
margin-top: auto !important;
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.bg-f5 {
|
||||
|
@ -120,14 +122,6 @@ a {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-zoom-out {
|
||||
cursor: zoom-out !important;
|
||||
}
|
||||
|
||||
img:not(a img, img.broken) {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.resize-none {
|
||||
resize: none;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <HttpErrorContent httpCode="50X" />;
|
||||
const [searchParams] = useSearchParams();
|
||||
const errMsg = searchParams.get('msg') || '';
|
||||
const title = searchParams.get('title') || '';
|
||||
return (
|
||||
<HttpErrorContent
|
||||
httpCode="50X"
|
||||
title={title}
|
||||
errMsg={errMsg}
|
||||
showErrorCode={!errMsg}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||
import { JSONSchema, SchemaForm, UISchema, ImgViewer } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { brandSetting, getBrandSetting } from '@/services';
|
||||
import { brandingStore } from '@/stores';
|
||||
|
@ -142,7 +142,7 @@ const Index: FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ImgViewer>
|
||||
<h3 className="mb-4">{t('page_title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
|
@ -151,7 +151,7 @@ const Index: FC = () => {
|
|||
onSubmit={onSubmit}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</div>
|
||||
</ImgViewer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +31,11 @@ const Index: FC = () => {
|
|||
title: t('header.label'),
|
||||
description: t('header.text'),
|
||||
},
|
||||
custom_sidebar: {
|
||||
type: 'string',
|
||||
title: t('sidebar.label'),
|
||||
description: t('sidebar.text'),
|
||||
},
|
||||
custom_footer: {
|
||||
type: 'string',
|
||||
title: t('footer.label'),
|
||||
|
@ -60,6 +65,13 @@ const Index: FC = () => {
|
|||
className: ['fs-14', 'font-monospace'],
|
||||
},
|
||||
},
|
||||
custom_sidebar: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
rows: 10,
|
||||
className: ['fs-14', 'font-monospace'],
|
||||
},
|
||||
},
|
||||
custom_footer: {
|
||||
'ui:widget': 'textarea',
|
||||
'ui:options': {
|
||||
|
@ -77,6 +89,7 @@ const Index: FC = () => {
|
|||
custom_css: formData.custom_css.value,
|
||||
custom_head: formData.custom_head.value,
|
||||
custom_header: formData.custom_header.value,
|
||||
custom_sidebar: formData.custom_sidebar.value,
|
||||
custom_footer: formData.custom_footer.value,
|
||||
};
|
||||
|
||||
|
@ -103,6 +116,7 @@ const Index: FC = () => {
|
|||
formMeta.custom_css.value = setting.custom_css;
|
||||
formMeta.custom_head.value = setting.custom_head;
|
||||
formMeta.custom_header.value = setting.custom_header;
|
||||
formMeta.custom_sidebar.value = setting.custom_sidebar;
|
||||
formMeta.custom_footer.value = setting.custom_footer;
|
||||
setFormData(formMeta);
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ const Flags: FC = () => {
|
|||
{flagTypeKeys.map((li) => {
|
||||
return (
|
||||
<option value={li} key={li}>
|
||||
{li}
|
||||
{t(li, { keyPrefix: 'btns' })}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
|
@ -96,7 +96,9 @@ const Flags: FC = () => {
|
|||
<td>
|
||||
<Stack>
|
||||
<small className="text-secondary">
|
||||
Flagged {li.object_type}
|
||||
{t('flagged_type', {
|
||||
type: t(li.object_type, { keyPrefix: 'btns' }),
|
||||
})}
|
||||
</small>
|
||||
<BaseUserCard
|
||||
data={li.reported_user}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@/common/interface';
|
||||
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||
import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
||||
import { DEFAULT_TIMEZONE } from '@/common/constants';
|
||||
import {
|
||||
updateInterfaceSetting,
|
||||
useInterfaceSetting,
|
||||
|
@ -48,13 +48,6 @@ const Interface: FC = () => {
|
|||
description: t('time_zone.text'),
|
||||
default: setting?.time_zone || DEFAULT_TIMEZONE,
|
||||
},
|
||||
default_avatar: {
|
||||
type: 'string',
|
||||
title: t('avatar.label'),
|
||||
description: t('avatar.text'),
|
||||
enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
|
||||
enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -69,11 +62,6 @@ const Interface: FC = () => {
|
|||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
default_avatar: {
|
||||
value: setting?.default_avatar || 'system',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
|
||||
const uiSchema: UISchema = {
|
||||
|
@ -83,9 +71,6 @@ const Interface: FC = () => {
|
|||
time_zone: {
|
||||
'ui:widget': 'timezone',
|
||||
},
|
||||
default_avatar: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
};
|
||||
const getLangs = async () => {
|
||||
const res: LangsType[] = await loadLanguageOptions(true);
|
||||
|
@ -118,7 +103,6 @@ const Interface: FC = () => {
|
|||
const reqParams: AdminSettingsInterface = {
|
||||
language: formData.language.value,
|
||||
time_zone: formData.time_zone.value,
|
||||
default_avatar: formData.default_avatar.value,
|
||||
};
|
||||
|
||||
updateInterfaceSetting(reqParams)
|
||||
|
@ -147,9 +131,6 @@ const Interface: FC = () => {
|
|||
const formMeta = {};
|
||||
Object.keys(setting).forEach((k) => {
|
||||
formMeta[k] = { ...formData[k], value: setting[k] };
|
||||
if (k === 'default_avatar') {
|
||||
formMeta[k].value = setting[k] || 'system';
|
||||
}
|
||||
});
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
}
|
||||
|
|
|
@ -22,6 +22,17 @@ const Index: FC = () => {
|
|||
description: t('membership.text'),
|
||||
default: false,
|
||||
},
|
||||
allow_email_registrations: {
|
||||
type: 'boolean',
|
||||
title: t('email_registration.title'),
|
||||
description: t('email_registration.text'),
|
||||
default: true,
|
||||
},
|
||||
allow_email_domains: {
|
||||
type: 'string',
|
||||
title: t('allowed_email_domains.title'),
|
||||
description: t('allowed_email_domains.text'),
|
||||
},
|
||||
login_required: {
|
||||
type: 'boolean',
|
||||
title: t('private.title'),
|
||||
|
@ -37,6 +48,15 @@ const Index: FC = () => {
|
|||
label: t('membership.label'),
|
||||
},
|
||||
},
|
||||
allow_email_registrations: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('email_registration.label'),
|
||||
},
|
||||
},
|
||||
allow_email_domains: {
|
||||
'ui:widget': 'textarea',
|
||||
},
|
||||
login_required: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
|
@ -51,8 +71,20 @@ const Index: FC = () => {
|
|||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
|
||||
const allowedEmailDomains: string[] = [];
|
||||
if (formData.allow_email_domains.value) {
|
||||
const domainList = formData.allow_email_domains.value.split('\n');
|
||||
domainList.forEach((li) => {
|
||||
li = li.trim();
|
||||
if (li) {
|
||||
allowedEmailDomains.push(li);
|
||||
}
|
||||
});
|
||||
}
|
||||
const reqParams: Type.AdminSettingsLogin = {
|
||||
allow_new_registrations: formData.allow_new_registrations.value,
|
||||
allow_email_registrations: formData.allow_email_registrations.value,
|
||||
allow_email_domains: allowedEmailDomains,
|
||||
login_required: formData.login_required.value,
|
||||
};
|
||||
|
||||
|
@ -78,6 +110,13 @@ const Index: FC = () => {
|
|||
const formMeta = { ...formData };
|
||||
formMeta.allow_new_registrations.value =
|
||||
setting.allow_new_registrations;
|
||||
formMeta.allow_email_registrations.value =
|
||||
setting.allow_email_registrations;
|
||||
formMeta.allow_email_domains.value = '';
|
||||
if (Array.isArray(setting.allow_email_domains)) {
|
||||
formMeta.allow_email_domains.value =
|
||||
setting.allow_email_domains.join('\n');
|
||||
}
|
||||
formMeta.login_required.value = setting.login_required;
|
||||
setFormData({ ...formMeta });
|
||||
}
|
||||
|
|
|
@ -2,18 +2,23 @@ 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 { SchemaForm, JSONSchema, UISchema } from '@/components';
|
||||
import { useQueryPluginConfig, updatePluginConfig } from '@/services';
|
||||
import { InputOptions } from '@/components/SchemaForm';
|
||||
import {
|
||||
InputOptions,
|
||||
FormKit,
|
||||
initFormData,
|
||||
mergeFormData,
|
||||
} 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 { data, mutate: refreshPluginConfig } = useQueryPluginConfig({
|
||||
plugin_slug_name: slug_name,
|
||||
});
|
||||
const Toast = useToast();
|
||||
const [schema, setSchema] = useState<JSONSchema | null>(null);
|
||||
const [uiSchema, setUISchema] = useState<UISchema>();
|
||||
|
@ -62,7 +67,7 @@ const Config = () => {
|
|||
};
|
||||
setSchema(result);
|
||||
setUISchema(uiConf);
|
||||
setFormData(initFormData(result));
|
||||
setFormData(mergeFormData(initFormData(result), formData));
|
||||
}, [data?.config_fields]);
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
|
@ -86,24 +91,19 @@ const Config = () => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
const refreshConfig: FormKit['refreshConfig'] = async () => {
|
||||
refreshPluginConfig();
|
||||
};
|
||||
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}
|
||||
refreshConfig={refreshConfig}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
onChange={handleOnChange}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { FC, FormEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useToast } from '@/hooks';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
|
||||
import {
|
||||
getPrivilegeSetting,
|
||||
putPrivilegeSetting,
|
||||
AdminSettingsPrivilege,
|
||||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.privilege',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const [privilege, setPrivilege] = useState<AdminSettingsPrivilege>();
|
||||
const [schema, setSchema] = useState<JSONSchema>({
|
||||
title: t('title'),
|
||||
properties: {},
|
||||
});
|
||||
const [uiSchema, setUiSchema] = useState<UISchema>({
|
||||
level: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
});
|
||||
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||
|
||||
const setFormConfig = (selectedLevel: number = 1) => {
|
||||
selectedLevel = Number(selectedLevel);
|
||||
const levelOptions = privilege?.options;
|
||||
const curLevel = levelOptions?.find((li) => {
|
||||
return li.level === selectedLevel;
|
||||
});
|
||||
if (!levelOptions || !curLevel) {
|
||||
return;
|
||||
}
|
||||
const uiState = {
|
||||
level: uiSchema.level,
|
||||
};
|
||||
const props: JSONSchema['properties'] = {
|
||||
level: {
|
||||
type: 'number',
|
||||
title: t('level.label'),
|
||||
description: t('level.text'),
|
||||
enum: levelOptions.map((_) => _.level),
|
||||
enumNames: levelOptions.map((_) => _.level_desc),
|
||||
default: selectedLevel,
|
||||
},
|
||||
};
|
||||
curLevel.privileges.forEach((li) => {
|
||||
props[li.key] = {
|
||||
type: 'number',
|
||||
title: li.label,
|
||||
default: li.value,
|
||||
};
|
||||
uiState[li.key] = {
|
||||
'ui:options': {
|
||||
readOnly: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
const schemaState = {
|
||||
...schema,
|
||||
properties: props,
|
||||
};
|
||||
const formState = initFormData(schemaState);
|
||||
curLevel.privileges.forEach((li) => {
|
||||
formState[li.key] = {
|
||||
value: li.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
});
|
||||
setSchema(schemaState);
|
||||
setUiSchema(uiState);
|
||||
setFormData(formState);
|
||||
};
|
||||
|
||||
const onSubmit = (evt: FormEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const lv = Number(formData.level.value);
|
||||
putPrivilegeSetting(lv)
|
||||
.then(() => {
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!privilege) {
|
||||
return;
|
||||
}
|
||||
setFormConfig(privilege.selected_level);
|
||||
}, [privilege]);
|
||||
useEffect(() => {
|
||||
getPrivilegeSetting().then((resp) => {
|
||||
setPrivilege(resp);
|
||||
});
|
||||
}, []);
|
||||
const handleOnChange = (state) => {
|
||||
setFormConfig(state.level.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -80,7 +80,7 @@ const Index: FC = () => {
|
|||
formMeta.robots.value = setting.robots;
|
||||
formMeta.permalink.value = setting.permalink;
|
||||
if (!/[1234]/.test(formMeta.permalink.value)) {
|
||||
formMeta.permalink.value = 1;
|
||||
formMeta.permalink.value = 4;
|
||||
}
|
||||
setFormData(formMeta);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
import { FC, FormEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useToast } from '@/hooks';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { JSONSchema, SchemaForm, UISchema, initFormData } from '@/components';
|
||||
import { SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
||||
import {
|
||||
getUsersSetting,
|
||||
putUsersSetting,
|
||||
AdminSettingsUsers,
|
||||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import * as Type from '@/common/interface';
|
||||
import { siteInfoStore } from '@/stores';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'admin.settings_users',
|
||||
});
|
||||
const Toast = useToast();
|
||||
const { updateUsers: updateUsersStore } = siteInfoStore();
|
||||
const schema: JSONSchema = {
|
||||
title: t('title'),
|
||||
properties: {
|
||||
default_avatar: {
|
||||
type: 'string',
|
||||
title: t('avatar.label'),
|
||||
description: t('avatar.text'),
|
||||
enum: SYSTEM_AVATAR_OPTIONS?.map((v) => v.value),
|
||||
enumNames: SYSTEM_AVATAR_OPTIONS?.map((v) => v.label),
|
||||
default: 'system',
|
||||
},
|
||||
profile_editable: {
|
||||
type: 'string',
|
||||
title: t('profile_editable.title'),
|
||||
},
|
||||
allow_update_display_name: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_display_name',
|
||||
},
|
||||
allow_update_username: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_username',
|
||||
},
|
||||
allow_update_avatar: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_avatar',
|
||||
},
|
||||
allow_update_bio: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_bio',
|
||||
},
|
||||
allow_update_website: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_website',
|
||||
},
|
||||
allow_update_location: {
|
||||
type: 'boolean',
|
||||
title: 'allow_update_location',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
|
||||
|
||||
const uiSchema: UISchema = {
|
||||
default_avatar: {
|
||||
'ui:widget': 'select',
|
||||
},
|
||||
profile_editable: {
|
||||
'ui:widget': 'legend',
|
||||
},
|
||||
allow_update_display_name: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_display_name.label'),
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
allow_update_username: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_username.label'),
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
allow_update_avatar: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_avatar.label'),
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
allow_update_bio: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_bio.label'),
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
allow_update_website: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_website.label'),
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
allow_update_location: {
|
||||
'ui:widget': 'switch',
|
||||
'ui:options': {
|
||||
label: t('allow_update_location.label'),
|
||||
fieldClassName: 'mb-3',
|
||||
simplify: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const onSubmit = (evt: FormEvent) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const reqParams: AdminSettingsUsers = {
|
||||
allow_update_avatar: formData.allow_update_avatar.value,
|
||||
allow_update_bio: formData.allow_update_bio.value,
|
||||
allow_update_display_name: formData.allow_update_display_name.value,
|
||||
allow_update_location: formData.allow_update_location.value,
|
||||
allow_update_username: formData.allow_update_username.value,
|
||||
allow_update_website: formData.allow_update_website.value,
|
||||
default_avatar: formData.default_avatar.value,
|
||||
};
|
||||
putUsersSetting(reqParams)
|
||||
.then(() => {
|
||||
updateUsersStore(reqParams);
|
||||
Toast.onShow({
|
||||
msg: t('update', { keyPrefix: 'toast' }),
|
||||
variant: 'success',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUsersSetting().then((resp) => {
|
||||
if (!resp) {
|
||||
return;
|
||||
}
|
||||
const formMeta: Type.FormDataType = {};
|
||||
Object.keys(formData).forEach((k) => {
|
||||
let v = resp[k];
|
||||
if (k === 'default_avatar' && !v) {
|
||||
v = 'system';
|
||||
}
|
||||
formMeta[k] = { ...formData[k], value: v };
|
||||
});
|
||||
setFormData({ ...formData, ...formMeta });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleOnChange = (data) => {
|
||||
setFormData(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -1,4 +1,4 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Form, Table, Dropdown, Button, Stack } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -21,8 +21,14 @@ import {
|
|||
useChangePasswordModal,
|
||||
useToast,
|
||||
} from '@/hooks';
|
||||
import { useQueryUsers, addUser, updateUserPassword } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import {
|
||||
useQueryUsers,
|
||||
addUser,
|
||||
updateUserPassword,
|
||||
getAdminUcAgent,
|
||||
AdminUcAgent,
|
||||
} from '@/services';
|
||||
import { loggedUserInfoStore, userCenterStore } from '@/stores';
|
||||
import { formatCount } from '@/utils';
|
||||
|
||||
const UserFilterKeys: Type.UserFilterBy[] = [
|
||||
|
@ -49,6 +55,13 @@ const Users: FC = () => {
|
|||
const curPage = Number(urlSearchParams.get('page') || '1');
|
||||
const curQuery = urlSearchParams.get('query') || '';
|
||||
const currentUser = loggedUserInfoStore((state) => state.user);
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [adminUcAgent, setAdminUcAgent] = useState<AdminUcAgent>({
|
||||
allow_create_user: true,
|
||||
allow_update_user_status: true,
|
||||
allow_update_user_password: true,
|
||||
allow_update_user_role: true,
|
||||
});
|
||||
const Toast = useToast();
|
||||
const {
|
||||
data,
|
||||
|
@ -138,6 +151,25 @@ const Users: FC = () => {
|
|||
urlSearchParams.delete('page');
|
||||
setUrlSearchParams(urlSearchParams);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (ucAgent?.enabled) {
|
||||
getAdminUcAgent().then((resp) => {
|
||||
setAdminUcAgent(resp);
|
||||
});
|
||||
}
|
||||
}, [ucAgent]);
|
||||
const showAddUser =
|
||||
!ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);
|
||||
const showActionPassword =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_password);
|
||||
const showActionRole =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_role);
|
||||
const showActionStatus =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_status);
|
||||
const showAction = showActionPassword || showActionRole || showActionStatus;
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
|
@ -149,12 +181,14 @@ const Users: FC = () => {
|
|||
sortKey="filter"
|
||||
i18nKeyPrefix="admin.users"
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => userModal.onShow()}>
|
||||
{t('add_user')}
|
||||
</Button>
|
||||
{showAddUser ? (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
onClick={() => userModal.onShow()}>
|
||||
{t('add_user')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
<Form.Control
|
||||
|
@ -232,25 +266,31 @@ const Users: FC = () => {
|
|||
</span>
|
||||
</td>
|
||||
)}
|
||||
{curFilter !== 'deleted' ? (
|
||||
{curFilter !== 'deleted' && showAction ? (
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('password', user)}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', user)}>
|
||||
{t('change_role')}
|
||||
</Dropdown.Item>
|
||||
{showActionPassword ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('password', user)}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{showActionStatus ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{showActionRole ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', user)}>
|
||||
{t('change_role')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</td>
|
||||
|
|
|
@ -19,7 +19,7 @@ const g10Paths = [
|
|||
'answers',
|
||||
'users',
|
||||
'flags',
|
||||
'installed_plugins',
|
||||
'installed-plugins',
|
||||
];
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||
|
@ -80,7 +80,7 @@ const Index: FC = () => {
|
|||
<Col lg={2}>
|
||||
<AccordionNav menus={menus} path="/admin/" />
|
||||
</Col>
|
||||
<Col lg={g10Paths.find((v) => curPath.includes(v)) ? 10 : 6}>
|
||||
<Col lg={g10Paths.find((v) => curPath === v) ? 10 : 6}>
|
||||
<Outlet />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
HttpErrorContent,
|
||||
} from '@/components';
|
||||
import { LoginToContinueModal } from '@/components/Modal';
|
||||
import { useImgViewer } from '@/hooks';
|
||||
|
||||
const Layout: FC = () => {
|
||||
const location = useLocation();
|
||||
|
@ -24,7 +23,6 @@ const Layout: FC = () => {
|
|||
toastClear();
|
||||
};
|
||||
const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore();
|
||||
const imgViewer = useImgViewer();
|
||||
const { show: showLoginToContinueModal } = loginToContinueStore();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -40,9 +38,7 @@ const Layout: FC = () => {
|
|||
}}>
|
||||
<Header />
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
<div
|
||||
className="position-relative page-wrap"
|
||||
onClick={imgViewer.checkClickForImgView}>
|
||||
<div className="position-relative page-wrap">
|
||||
{httpStatusCode ? (
|
||||
<HttpErrorContent httpCode={httpStatusCode} />
|
||||
) : (
|
||||
|
|
|
@ -307,7 +307,7 @@ const Ask = () => {
|
|||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.content.value = revision.content.content;
|
||||
formData.content.value = revision.content?.content || '';
|
||||
setImmData({ ...formData });
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Comment,
|
||||
FormatTime,
|
||||
htmlRender,
|
||||
ImgViewer,
|
||||
} from '@/components';
|
||||
import { scrollToElementTop, bgFadeOut } from '@/utils';
|
||||
import { AnswerItem } from '@/common/interface';
|
||||
|
@ -84,10 +85,12 @@ const Index: FC<Props> = ({
|
|||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<article
|
||||
className="fmt text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
<ImgViewer>
|
||||
<article
|
||||
className="fmt text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
</ImgViewer>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<Actions
|
||||
source="answer"
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
Icon,
|
||||
ImgViewer,
|
||||
} from '@/components';
|
||||
import { formatCount, guard } from '@/utils';
|
||||
import { following } from '@/services';
|
||||
|
@ -114,11 +115,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
|||
return <Tag className="m-1" key={item.slug_name} data={item} />;
|
||||
})}
|
||||
</div>
|
||||
<article
|
||||
ref={ref}
|
||||
className="fmt text-break text-wrap mt-4"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
<ImgViewer>
|
||||
<article
|
||||
ref={ref}
|
||||
className="fmt text-break text-wrap mt-4"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
</ImgViewer>
|
||||
|
||||
<Actions
|
||||
className="mt-4"
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Pagination } from '@/components';
|
||||
import { Pagination, CustomSidebar } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollToElementTop } from '@/utils';
|
||||
import { usePageTags, usePageUsers } from '@/hooks';
|
||||
|
@ -83,7 +83,7 @@ const Index = () => {
|
|||
// delete answers only show to author and admin and has search params aid
|
||||
if (v.status === 10) {
|
||||
if (
|
||||
(v?.user_info.username === userInfo?.username || isAdmin) &&
|
||||
(v?.user_info?.username === userInfo?.username || isAdmin) &&
|
||||
aid === v.id
|
||||
) {
|
||||
return v;
|
||||
|
@ -104,8 +104,8 @@ const Index = () => {
|
|||
res.list.forEach((item) => {
|
||||
setUsers([
|
||||
{
|
||||
displayName: item.user_info.display_name,
|
||||
userName: item.user_info.username,
|
||||
displayName: item.user_info?.display_name,
|
||||
userName: item.user_info?.username,
|
||||
},
|
||||
{
|
||||
displayName: item?.update_user_info?.display_name,
|
||||
|
@ -123,10 +123,10 @@ const Index = () => {
|
|||
if (res) {
|
||||
setUsers([
|
||||
{
|
||||
id: res.user_info.id,
|
||||
displayName: res.user_info.display_name,
|
||||
userName: res.user_info.username,
|
||||
avatar_url: res.user_info.avatar,
|
||||
id: res.user_info?.id,
|
||||
displayName: res.user_info?.display_name,
|
||||
userName: res.user_info?.username,
|
||||
avatar_url: res.user_info?.avatar,
|
||||
},
|
||||
{
|
||||
id: res?.update_user_info?.id,
|
||||
|
@ -256,6 +256,7 @@ const Index = () => {
|
|||
)}
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<CustomSidebar />
|
||||
<RelatedQuestions id={question?.id || ''} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -4,10 +4,20 @@ import { useMatch, Link, useSearchParams } from 'react-router-dom';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { FollowingTags, QuestionList, HotQuestions } from '@/components';
|
||||
import { siteInfoStore, loggedUserInfoStore } from '@/stores';
|
||||
import {
|
||||
FollowingTags,
|
||||
QuestionList,
|
||||
HotQuestions,
|
||||
CustomSidebar,
|
||||
} from '@/components';
|
||||
import {
|
||||
siteInfoStore,
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
} from '@/stores';
|
||||
import { useQuestionList } from '@/services';
|
||||
import * as Type from '@/common/interface';
|
||||
import { userCenter, floppyNavigation } from '@/utils';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
|
@ -30,6 +40,7 @@ const Questions: FC = () => {
|
|||
pageTitle = `${siteInfo.name}`;
|
||||
slogan = `${siteInfo.short_description}`;
|
||||
}
|
||||
const { login: loginSetting } = loginSettingStore();
|
||||
|
||||
usePageTags({ title: pageTitle, subtitle: slogan });
|
||||
return (
|
||||
|
@ -43,7 +54,8 @@ const Questions: FC = () => {
|
|||
/>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
{!loggedUser.access_token && (
|
||||
<CustomSidebar />
|
||||
{!loggedUser.username && (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">
|
||||
|
@ -52,12 +64,20 @@ const Questions: FC = () => {
|
|||
})}
|
||||
</h5>
|
||||
<p className="card-text">{siteInfo.description}</p>
|
||||
<Link to="/users/login" className="btn btn-primary">
|
||||
<Link
|
||||
to={userCenter.getLoginUrl()}
|
||||
className="btn btn-primary"
|
||||
onClick={floppyNavigation.handleRouteLinkClick}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
<Link to="/users/register" className="btn btn-link ms-2">
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
{loginSetting.allow_new_registrations ? (
|
||||
<Link
|
||||
to={userCenter.getSignUpUrl()}
|
||||
className="btn btn-link ms-2"
|
||||
onClick={floppyNavigation.handleRouteLinkClick}>
|
||||
{t('signup', { keyPrefix: 'btns' })}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import * as Type from '@/common/interface';
|
||||
import { FollowingTags } from '@/components';
|
||||
import { FollowingTags, CustomSidebar } from '@/components';
|
||||
import {
|
||||
useTagInfo,
|
||||
useFollow,
|
||||
|
@ -152,6 +152,7 @@ const Questions: FC = () => {
|
|||
<QuestionList source="tag" data={listData} isLoading={listLoading} />
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
<CustomSidebar />
|
||||
<FollowingTags />
|
||||
<HotQuestions />
|
||||
</Col>
|
||||
|
|
|
@ -64,8 +64,8 @@ const Tags = () => {
|
|||
<Row className="mb-4 d-flex justify-content-center">
|
||||
<Col xxl={10} sm={12}>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
<div className="d-block d-sm-flex justify-content-between align-items-center flex-wrap">
|
||||
<Stack direction="horizontal" gap={3} className="mb-3 mb-sm-0">
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import React, { memo, FC, useState, useEffect } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import QrCode from 'qrcode';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { guard, getUaType, floppyNavigation } from '@/utils';
|
||||
import { USER_AGENT_NAMES } from '@/common/constants';
|
||||
|
||||
import { getLoginConf, checkLoginResult } from './service';
|
||||
|
||||
let checkTimer: NodeJS.Timeout;
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const navigate = useNavigate();
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const agentName = ucAgent?.agent_info?.name || '';
|
||||
const [qrcodeDataUrl, setQrCodeDataUrl] = useState('');
|
||||
const handleLoginResult = (key: string) => {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
checkLoginResult(key).then((res) => {
|
||||
if (res.is_login) {
|
||||
guard.handleLoginWithToken(res.token, navigate);
|
||||
return;
|
||||
}
|
||||
clearTimeout(checkTimer);
|
||||
checkTimer = setTimeout(() => {
|
||||
handleLoginResult(key);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
const handleQrCode = (targetUrl: string) => {
|
||||
if (!targetUrl) {
|
||||
return;
|
||||
}
|
||||
QrCode.toDataURL(targetUrl, { width: 240, margin: 0 }, (err, url) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
setQrCodeDataUrl(url);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!agentName) {
|
||||
return;
|
||||
}
|
||||
getLoginConf().then((res) => {
|
||||
if (getUaType() === USER_AGENT_NAMES.WeCom) {
|
||||
floppyNavigation.navigate(res?.redirect_url, {
|
||||
handler: 'replace',
|
||||
});
|
||||
} else {
|
||||
handleQrCode(res?.redirect_url);
|
||||
handleLoginResult(res?.key);
|
||||
}
|
||||
});
|
||||
}, [agentName]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeout(checkTimer);
|
||||
};
|
||||
}, []);
|
||||
if (getUaType() !== USER_AGENT_NAMES.WeCom) {
|
||||
return (
|
||||
<Card className="text-center">
|
||||
<Card.Body>
|
||||
<Card.Title as="h3" className="mb-3">
|
||||
{ucAgent?.agent_info.display_name} {t('login')}
|
||||
</Card.Title>
|
||||
{qrcodeDataUrl ? (
|
||||
<>
|
||||
<img
|
||||
className="w-100"
|
||||
style={{ maxWidth: '240px' }}
|
||||
src={qrcodeDataUrl}
|
||||
alt={agentName}
|
||||
/>
|
||||
<div className="text-secondary mt-3">
|
||||
{t('qrcode_login_tip', {
|
||||
agentName: ucAgent?.agent_info.display_name,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,21 @@
|
|||
import request from '@/utils/request';
|
||||
|
||||
type loginConf = {
|
||||
key: string;
|
||||
redirect_url: string;
|
||||
};
|
||||
|
||||
type loginResult = {
|
||||
is_login: boolean;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const getLoginConf = () => {
|
||||
const apiUrl = `/answer/api/v1/wecom/login/url`;
|
||||
return request.get<loginConf>(apiUrl);
|
||||
};
|
||||
|
||||
export const checkLoginResult = (key: loginConf['key']) => {
|
||||
const apiUrl = `/answer/api/v1/wecom/login/check?key=${key}`;
|
||||
return request.get<loginResult>(apiUrl);
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { memo } from 'react';
|
||||
import { Container, Col } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { USER_AGENT_NAMES } from '@/common/constants';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
import WeComAuth from './components/WeCom';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
const [searchParam] = useSearchParams();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
let agentName = ucAgent?.agent_info.name || '';
|
||||
if (searchParam.get('agent_name')) {
|
||||
agentName = searchParam.get('agent_name') || '';
|
||||
}
|
||||
usePageTags({
|
||||
title: t('login', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '5rem', paddingBottom: '5rem' }}>
|
||||
<Col md={4} className="mx-auto">
|
||||
{USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (
|
||||
<WeComAuth />
|
||||
) : null}
|
||||
</Col>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,61 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Card, Col, Carousel } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 1,
|
||||
url: require('@/assets/images/carousel-wecom-1.jpg'),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
url: require('@/assets/images/carousel-wecom-2.jpg'),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
url: require('@/assets/images/carousel-wecom-3.jpg'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
url: require('@/assets/images/carousel-wecom-4.jpg'),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
url: require('@/assets/images/carousel-wecom-5.jpg'),
|
||||
},
|
||||
];
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const ucAgent = userCenterStore().agent;
|
||||
return (
|
||||
<Col lg={4} className="mx-auto mt-3 py-5">
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<h3 className="text-center pt-3 mb-3">
|
||||
{ucAgent?.agent_info.display_name} {t('login')}
|
||||
</h3>
|
||||
<p className="text-danger text-center">
|
||||
{t('login_failed_email_tip')}
|
||||
</p>
|
||||
|
||||
<Carousel controls={false}>
|
||||
{data.map((item) => (
|
||||
<Carousel.Item key={item.id}>
|
||||
<img
|
||||
className="d-block w-100"
|
||||
src={item.url}
|
||||
alt="First slide"
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
</Carousel>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,32 @@
|
|||
import { memo } from 'react';
|
||||
import { Container } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { USER_AGENT_NAMES } from '@/common/constants';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
import WeCom from './components/WeCom';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
const [searchParam] = useSearchParams();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
let agentName = ucAgent?.agent_info.name || '';
|
||||
if (searchParam.get('agent_name')) {
|
||||
agentName = searchParam.get('agent_name') || '';
|
||||
}
|
||||
usePageTags({
|
||||
title: t('login', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container>
|
||||
{USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (
|
||||
<WeCom />
|
||||
) : null}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -22,7 +22,7 @@ const Index: React.FC = () => {
|
|||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<SendEmail visible={step === 1} callback={callback} />
|
||||
</Col>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { FC, memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_title' });
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
const token = searchParams.get('access_token');
|
||||
guard.handleLoginWithToken(token, navigate);
|
||||
}, []);
|
||||
usePageTags({
|
||||
title: t('oauth_callback'),
|
||||
});
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -15,7 +15,7 @@ const Index: FC = () => {
|
|||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<WelcomeTitle />
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<SendEmail />
|
||||
</Col>
|
||||
</Container>
|
||||
|
|
|
@ -4,8 +4,9 @@ import { Link, useSearchParams } from 'react-router-dom';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { loggedUserInfoStore, siteInfoStore } from '@/stores';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { changeEmailVerify } from '@/services';
|
||||
import { WelcomeTitle } from '@/components';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||
|
@ -13,7 +14,6 @@ const Index: FC = () => {
|
|||
const [step, setStep] = useState('loading');
|
||||
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get('code');
|
||||
|
@ -39,9 +39,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" />
|
||||
{step === 'success' && (
|
||||
<>
|
||||
<p className="text-center">{t('confirm_new_email')}</p>
|
||||
|
|
|
@ -3,8 +3,6 @@ import { Container, Form, Button, Col } from 'react-bootstrap';
|
|||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { REDIRECT_PATH_STORAGE_KEY } from '@/common/constants';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import type {
|
||||
LoginReqParams,
|
||||
|
@ -12,12 +10,15 @@ import type {
|
|||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
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 { PluginOauth, PluginUcLogin } from '@/plugins';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
userCenterStore,
|
||||
} from '@/stores';
|
||||
import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils';
|
||||
import { login, checkImgCode, UcAgent } from '@/services';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'login' });
|
||||
|
@ -26,6 +27,14 @@ const Index: React.FC = () => {
|
|||
const [refresh, setRefresh] = useState(0);
|
||||
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
|
||||
const loginSetting = loginSettingStore((state) => state.login);
|
||||
const ucAgent = userCenterStore().agent;
|
||||
let ucAgentInfo: UcAgent['agent_info'] | undefined;
|
||||
if (ucAgent?.enabled && ucAgent?.agent_info) {
|
||||
ucAgentInfo = ucAgent.agent_info;
|
||||
}
|
||||
const canOriginalLogin =
|
||||
!ucAgentInfo || ucAgentInfo.enabled_original_user_system;
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
e_mail: {
|
||||
value: '',
|
||||
|
@ -56,6 +65,9 @@ const Index: React.FC = () => {
|
|||
};
|
||||
|
||||
const getImgCode = () => {
|
||||
if (!canOriginalLogin) {
|
||||
return;
|
||||
}
|
||||
checkImgCode({
|
||||
action: 'login',
|
||||
}).then((res) => {
|
||||
|
@ -91,14 +103,6 @@ const Index: React.FC = () => {
|
|||
return bol;
|
||||
};
|
||||
|
||||
const handleLoginRedirect = () => {
|
||||
const redirect = Storage.get(REDIRECT_PATH_STORAGE_KEY) || RouteAlias.home;
|
||||
Storage.remove(REDIRECT_PATH_STORAGE_KEY);
|
||||
floppyNavigation.navigate(redirect, () => {
|
||||
navigate(redirect, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogin = (event?: any) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
|
@ -121,7 +125,7 @@ const Index: React.FC = () => {
|
|||
setStep(2);
|
||||
setRefresh((pre) => pre + 1);
|
||||
} else {
|
||||
handleLoginRedirect();
|
||||
guard.handleLoginRedirect(navigate);
|
||||
}
|
||||
|
||||
setModalState(false);
|
||||
|
@ -171,9 +175,13 @@ const Index: React.FC = () => {
|
|||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
{step === 1 && canOriginalLogin ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
{ucAgentInfo ? (
|
||||
<PluginUcLogin className="mb-5" />
|
||||
) : (
|
||||
<PluginOauth className="mb-5" />
|
||||
)}
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('email.label')}</Form.Label>
|
||||
|
@ -238,14 +246,17 @@ const Index: React.FC = () => {
|
|||
<div className="text-center mt-5">
|
||||
<Trans i18nKey="login.info_sign" ns="translation">
|
||||
Don’t have an account?
|
||||
<Link to="/users/register" tabIndex={2}>
|
||||
<Link
|
||||
to={userCenter.getSignUpUrl()}
|
||||
tabIndex={2}
|
||||
onClick={floppyNavigation.handleRouteLinkClick}>
|
||||
Sign up
|
||||
</Link>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{step === 2 && <Unactivate visible={step === 2} />}
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ const Index: FC = () => {
|
|||
<p>{t('info', { keyPrefix: 'inactive' })}</p>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<div className="text-center mb-5">{t('subtitle')}</div>
|
||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
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);
|
|
@ -119,7 +119,7 @@ const Index: React.FC = () => {
|
|||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('password.label')}</Form.Label>
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar, Icon } from '@/components';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { Avatar, Icon, SvgIcon } from '@/components';
|
||||
import type { UserInfoRes } from '@/common/interface';
|
||||
import { getUcBranding, UcBrandingEntry } from '@/services';
|
||||
import { userCenterStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
data: UserInfoRes;
|
||||
|
@ -12,6 +16,22 @@ interface Props {
|
|||
|
||||
const Index: FC<Props> = ({ data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [ucBranding, setUcBranding] = useState<UcBrandingEntry[]>([]);
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled && data?.username) {
|
||||
getUcBranding(data.username).then((resp) => {
|
||||
if (resp.enabled && Array.isArray(resp.personal_branding)) {
|
||||
setUcBranding(resp.personal_branding);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, [data?.username]);
|
||||
if (!data?.username) {
|
||||
return null;
|
||||
}
|
||||
|
@ -65,27 +85,55 @@ const Index: FC<Props> = ({ data }) => {
|
|||
</div>
|
||||
|
||||
<div className="d-flex text-secondary">
|
||||
{data.location && (
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="geo-alt-fill" className="me-2" />
|
||||
<span>{data.location}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.website && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon name="house-door-fill" className="me-2" />
|
||||
<a
|
||||
className="link-secondary"
|
||||
href={
|
||||
data.website?.includes('http')
|
||||
? data.website
|
||||
: `http://${data.website}`
|
||||
}>
|
||||
{data?.website.replace(/(http|https):\/\//, '').split('/')?.[0]}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!ucAgent?.enabled ? (
|
||||
<>
|
||||
{data.location && (
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="geo-alt-fill" className="me-2" />
|
||||
<span>{data.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.website && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon name="house-door-fill" className="me-2" />
|
||||
<a
|
||||
className="link-secondary"
|
||||
href={
|
||||
data.website?.includes('http')
|
||||
? data.website
|
||||
: `http://${data.website}`
|
||||
}>
|
||||
{
|
||||
data?.website
|
||||
.replace(/(http|https):\/\//, '')
|
||||
.split('/')?.[0]
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{ucBranding.map((b, i, a) => {
|
||||
if (!b.label) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={b.name}
|
||||
className={classnames('d-flex', 'align-items-center', {
|
||||
'me-3': i < a.length - 1,
|
||||
})}>
|
||||
{b.icon ? <SvgIcon base64={b.icon} /> : null}
|
||||
{b.url ? (
|
||||
<a className="link-secondary" href={b.url}>
|
||||
{b.label}
|
||||
</a>
|
||||
) : (
|
||||
<span>{b.label}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FC } from 'react';
|
||||
import { Container, Row, Col, Button } from 'react-bootstrap';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useParams, useSearchParams, Link } from 'react-router-dom';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Pagination, FormatTime, Empty } from '@/components';
|
||||
|
@ -47,14 +47,16 @@ const Personal: FC = () => {
|
|||
},
|
||||
tabName,
|
||||
);
|
||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||
|
||||
let pageTitle = '';
|
||||
if (userInfo?.username) {
|
||||
pageTitle = `${userInfo?.display_name} (${userInfo?.username})`;
|
||||
}
|
||||
const { count = 0, list = [] } = listData?.[tabName] || {};
|
||||
usePageTags({
|
||||
title: pageTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
|
@ -71,12 +73,11 @@ const Personal: FC = () => {
|
|||
className="d-flex justify-content-start justify-content-md-end">
|
||||
{isSelf && (
|
||||
<div className="mb-3">
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
href="/users/settings/profile"
|
||||
className="btn">
|
||||
<Link
|
||||
className="btn btn-outline-secondary"
|
||||
to="/users/settings/profile">
|
||||
{t('edit_profile')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
useLegalTos,
|
||||
useLegalPrivacy,
|
||||
} from '@/services';
|
||||
import userStore from '@/stores/loggedUserInfoStore';
|
||||
import userStore from '@/stores/loggedUserInfo';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
||||
|
@ -17,12 +18,15 @@ const Index: React.FC = () => {
|
|||
usePageTags({
|
||||
title: t('sign_up', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
if (!guard.singUpAgent().ok) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
|
||||
{showForm ? (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<SignUpForm callback={onStep} />
|
||||
</Col>
|
||||
|
|
|
@ -1,18 +1,43 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { userCenterStore } from '@/stores';
|
||||
import { getUcSettings, UcSettingAgent } from '@/services';
|
||||
|
||||
import { ModifyEmail, ModifyPassword, MyLogins } from './components';
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.account',
|
||||
});
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [accountAgent, setAccountAgent] = useState<UcSettingAgent>();
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled) {
|
||||
getUcSettings().then((resp) => {
|
||||
setAccountAgent(resp.account_setting_agent);
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
{accountAgent?.enabled && accountAgent?.redirect_url ? (
|
||||
<a href={accountAgent.redirect_url}>
|
||||
{t('goto_modify', { keyPrefix: 'settings' })}
|
||||
</a>
|
||||
) : null}
|
||||
{!ucAgent?.enabled || accountAgent?.enabled === false ? (
|
||||
<>
|
||||
<ModifyEmail />
|
||||
<ModifyPassword />
|
||||
<MyLogins />
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,10 +5,15 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import MD5 from 'md5';
|
||||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { UploadImg, Avatar, Icon } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { UploadImg, Avatar, Icon, ImgViewer } from '@/components';
|
||||
import { loggedUserInfoStore, userCenterStore, siteInfoStore } from '@/stores';
|
||||
import { useToast } from '@/hooks';
|
||||
import { modifyUserInfo, getLoggedUserInfo } from '@/services';
|
||||
import {
|
||||
modifyUserInfo,
|
||||
getLoggedUserInfo,
|
||||
getUcSettings,
|
||||
UcSettingAgent,
|
||||
} from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
|
@ -17,9 +22,11 @@ const Index: React.FC = () => {
|
|||
});
|
||||
const toast = useToast();
|
||||
const { user, update } = loggedUserInfoStore();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const { users: usersSetting } = siteInfoStore();
|
||||
const [mailHash, setMailHash] = useState('');
|
||||
const [count] = useState(0);
|
||||
|
||||
const [profileAgent, setProfileAgent] = useState<UcSettingAgent>();
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
display_name: {
|
||||
value: '',
|
||||
|
@ -243,228 +250,255 @@ const Index: React.FC = () => {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
// const refreshGravatar = () => {
|
||||
// setCount((pre) => pre + 1);
|
||||
// };
|
||||
|
||||
const initData = () => {
|
||||
if (ucAgent?.enabled) {
|
||||
getUcSettings().then((resp) => {
|
||||
setProfileAgent(resp.profile_setting_agent);
|
||||
if (resp.profile_setting_agent?.enabled === false) {
|
||||
getProfile();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getProfile();
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
getProfile();
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('heading')}</h3>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="displayName" className="mb-3">
|
||||
<Form.Label>{t('display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.display_name.value}
|
||||
isInvalid={formData.display_name.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
display_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.display_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
{profileAgent?.enabled && profileAgent?.redirect_url ? (
|
||||
<a href={profileAgent.redirect_url}>
|
||||
{t('goto_modify', { keyPrefix: 'settings' })}
|
||||
</a>
|
||||
) : null}
|
||||
{!ucAgent?.enabled || profileAgent?.enabled === false ? (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="displayName" className="mb-3">
|
||||
<Form.Label>{t('display_name.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
disabled={!usersSetting.allow_update_display_name}
|
||||
value={formData.display_name.value}
|
||||
isInvalid={formData.display_name.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
display_name: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.display_name.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="userName" className="mb-3">
|
||||
<Form.Label>{t('username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
value={formData.username.value}
|
||||
isInvalid={formData.username.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="userName" className="mb-3">
|
||||
<Form.Label>{t('username.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
disabled={!usersSetting.allow_update_username}
|
||||
value={formData.username.value}
|
||||
isInvalid={formData.username.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
username: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text as="div">{t('username.caption')}</Form.Text>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.username.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="avatar" className="mb-3">
|
||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||
<div className="mb-3">
|
||||
<Form.Select
|
||||
name="avatar.type"
|
||||
value={formData.avatar.type}
|
||||
onChange={handleAvatarChange}>
|
||||
<option value="gravatar" key="gravatar">
|
||||
{t('avatar.gravatar')}
|
||||
</option>
|
||||
<option value="default" key="default">
|
||||
{t('avatar.default')}
|
||||
</option>
|
||||
<option value="custom" key="custom">
|
||||
{t('avatar.custom')}
|
||||
</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
{formData.avatar.type === 'gravatar' && (
|
||||
<Stack>
|
||||
<Avatar
|
||||
size="160px"
|
||||
avatar={formData.avatar.gravatar}
|
||||
searchStr={`s=256&d=identicon${
|
||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||
}`}
|
||||
className="me-3 rounded"
|
||||
/>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.gravatar_text">
|
||||
You can change image on
|
||||
<a
|
||||
href="https://gravatar.com"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
gravatar.com
|
||||
</a>
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
<Form.Group controlId="avatar" className="mb-3">
|
||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||
<div className="mb-3">
|
||||
<Form.Select
|
||||
name="avatar.type"
|
||||
disabled={!usersSetting.allow_update_avatar}
|
||||
value={formData.avatar.type}
|
||||
onChange={handleAvatarChange}>
|
||||
<option value="gravatar" key="gravatar">
|
||||
{t('avatar.gravatar')}
|
||||
</option>
|
||||
<option value="default" key="default">
|
||||
{t('avatar.default')}
|
||||
</option>
|
||||
<option value="custom" key="custom">
|
||||
{t('avatar.custom')}
|
||||
</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<ImgViewer>
|
||||
<div className="d-flex">
|
||||
{formData.avatar.type === 'gravatar' && (
|
||||
<Stack>
|
||||
<Avatar
|
||||
size="160px"
|
||||
avatar={formData.avatar.gravatar}
|
||||
searchStr={`s=256&d=identicon${
|
||||
count > 0 ? `&t=${new Date().valueOf()}` : ''
|
||||
}`}
|
||||
className="me-3 rounded"
|
||||
/>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.gravatar_text">
|
||||
You can change image on
|
||||
<a
|
||||
href="https://gravatar.com"
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
gravatar.com
|
||||
</a>
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{formData.avatar.type === 'custom' && (
|
||||
<Stack>
|
||||
<Stack direction="horizontal" className="align-items-start">
|
||||
<Avatar
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
avatar={formData.avatar.custom}
|
||||
className="me-2 bg-gray-300 "
|
||||
/>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg type="avatar" uploadCallback={avatarUpload}>
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={removeCustomAvatar}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.text">
|
||||
You can upload your image.
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
{formData.avatar.type === 'default' && (
|
||||
<Avatar size="160px" avatar="" />
|
||||
)}
|
||||
</div>
|
||||
<Form.Control
|
||||
isInvalid={formData.avatar.isInvalid}
|
||||
className="d-none"
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.avatar.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
{formData.avatar.type === 'custom' && (
|
||||
<Stack>
|
||||
<Stack direction="horizontal" className="align-items-start">
|
||||
<Avatar
|
||||
size="160px"
|
||||
searchStr="s=256"
|
||||
avatar={formData.avatar.custom}
|
||||
className="me-2 bg-gray-300 "
|
||||
/>
|
||||
<ButtonGroup vertical className="fit-content">
|
||||
<UploadImg
|
||||
type="avatar"
|
||||
disabled={!usersSetting.allow_update_avatar}
|
||||
uploadCallback={avatarUpload}>
|
||||
<Icon name="cloud-upload" />
|
||||
</UploadImg>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
disabled={!usersSetting.allow_update_avatar}
|
||||
onClick={removeCustomAvatar}>
|
||||
<Icon name="trash" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Form.Text className="text-muted mt-1">
|
||||
<Trans i18nKey="settings.profile.avatar.text">
|
||||
You can upload your image.
|
||||
</Trans>
|
||||
</Form.Text>
|
||||
</Stack>
|
||||
)}
|
||||
{formData.avatar.type === 'default' && (
|
||||
<Avatar size="160px" avatar="" />
|
||||
)}
|
||||
</div>
|
||||
</ImgViewer>
|
||||
<Form.Control
|
||||
isInvalid={formData.avatar.isInvalid}
|
||||
className="d-none"
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.avatar.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="bio" className="mb-3">
|
||||
<Form.Label>
|
||||
{`${t('bio.label')} ${t('optional', {
|
||||
<Form.Group controlId="bio" className="mb-3">
|
||||
<Form.Label>
|
||||
{`${t('bio.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="font-monospace"
|
||||
required
|
||||
as="textarea"
|
||||
rows={5}
|
||||
disabled={!usersSetting.allow_update_bio}
|
||||
value={formData.bio.value}
|
||||
isInvalid={formData.bio.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
bio: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.bio.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="website" className="mb-3">
|
||||
<Form.Label>{`${t('website.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="font-monospace"
|
||||
required
|
||||
as="textarea"
|
||||
rows={5}
|
||||
value={formData.bio.value}
|
||||
isInvalid={formData.bio.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
bio: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.bio.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="url"
|
||||
placeholder={t('website.placeholder')}
|
||||
disabled={!usersSetting.allow_update_website}
|
||||
value={formData.website.value}
|
||||
isInvalid={formData.website.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
website: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.website.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="website" className="mb-3">
|
||||
<Form.Label>{`${t('website.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="url"
|
||||
placeholder={t('website.placeholder')}
|
||||
value={formData.website.value}
|
||||
isInvalid={formData.website.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
website: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.website.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{`${t('location.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
placeholder={t('location.placeholder')}
|
||||
disabled={!usersSetting.allow_update_location}
|
||||
value={formData.location.value}
|
||||
isInvalid={formData.location.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
location: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.location.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{`${t('location.label')} ${t('optional', {
|
||||
keyPrefix: 'form',
|
||||
})}`}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
type="text"
|
||||
placeholder={t('location.placeholder')}
|
||||
value={formData.location.value}
|
||||
isInvalid={formData.location.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
location: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.location.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_name')}
|
||||
</Button>
|
||||
</Form>
|
||||
<Button variant="primary" type="submit">
|
||||
{t('btn_name')}
|
||||
</Button>
|
||||
</Form>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Nav } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { NavLink, useMatch } from 'react-router-dom';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'settings.nav' });
|
||||
const settingMatch = useMatch('/users/settings/:setting');
|
||||
return (
|
||||
<Nav variant="pills" className="flex-column">
|
||||
<NavLink className="nav-link" to="/users/settings/profile">
|
||||
<NavLink
|
||||
className={({ isActive }) =>
|
||||
isActive || !settingMatch ? 'nav-link active' : 'nav-link'
|
||||
}
|
||||
to="/users/settings/profile">
|
||||
{t('profile')}
|
||||
</NavLink>
|
||||
<NavLink className="nav-link" to="/users/settings/notify">
|
||||
|
|
|
@ -1,62 +1,17 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { FC, memo } from 'react';
|
||||
import { Container, Row, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
import { getLoggedUserInfo } from '@/services';
|
||||
|
||||
import Nav from './components/Nav';
|
||||
|
||||
const Index: React.FC = () => {
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.profile',
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<FormDataType>({
|
||||
display_name: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
avatar: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
bio: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
website: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
location: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
const getProfile = () => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
if (res) {
|
||||
formData.display_name.value = res.display_name;
|
||||
formData.bio.value = res.bio;
|
||||
formData.avatar.value = res.avatar;
|
||||
formData.location.value = res.location;
|
||||
formData.website.value = res.website;
|
||||
setFormData({ ...formData });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getProfile();
|
||||
}, []);
|
||||
usePageTags({
|
||||
title: t('settings', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
@ -81,4 +36,4 @@ const Index: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default React.memo(Index);
|
||||
export default memo(Index);
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Suspended = () => {
|
||||
const { contact_email = '' } = siteInfoStore((state) => state.siteInfo);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'suspended' });
|
||||
usePageTags({
|
||||
title: t('account_suspended', { keyPrefix: 'page_title' }),
|
||||
|
@ -16,6 +19,9 @@ const Suspended = () => {
|
|||
<br />
|
||||
{t('end')}
|
||||
</p>
|
||||
<Button href={`mailto:${contact_email}`} variant="link">
|
||||
{t('contact_us')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import classnames from 'classnames';
|
||||
|
||||
import { useGetStartUseOauthConnector } from '@/services';
|
||||
import { base64ToSvg } from '@/utils';
|
||||
import { SvgIcon } from '@/components';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
@ -20,12 +20,7 @@ const Index: FC<Props> = ({ className }) => {
|
|||
{data?.map((item) => {
|
||||
return (
|
||||
<Button variant="outline-secondary" href={item.link} key={item.name}>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base64ToSvg(item.icon),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SvgIcon base64={item.icon} />
|
||||
<span>{t('connect', { auth_name: item.name })}</span>
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { SvgIcon } from '@/components';
|
||||
import { userCenterStore } from '@/stores';
|
||||
|
||||
interface Props {
|
||||
className?: classnames.Argument;
|
||||
}
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const ucLoginRedirect =
|
||||
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
|
||||
|
||||
if (ucLoginRedirect) {
|
||||
return (
|
||||
<Button
|
||||
className={classnames('w-100', className)}
|
||||
variant="outline-secondary"
|
||||
href={ucAgent?.agent_info.login_redirect_url}>
|
||||
<SvgIcon base64={ucAgent?.agent_info.icon} />
|
||||
<span>
|
||||
{t('connect', { auth_name: ucAgent?.agent_info.display_name })}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,3 +1,4 @@
|
|||
import PluginOauth from './PluginOauth';
|
||||
import PluginUcLogin from './PluginUcLogin';
|
||||
|
||||
export { PluginOauth };
|
||||
export { PluginOauth, PluginUcLogin };
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
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[];
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { FC, ReactNode, useEffect } from 'react';
|
||||
import { useLocation, useNavigate, useLoaderData } from 'react-router-dom';
|
||||
import { FC, ReactNode, useEffect, useState } from 'react';
|
||||
import { useNavigate, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { TGuardFunc } from '@/utils/guard';
|
||||
import { TGuardFunc, TGuardResult } from '@/utils/guard';
|
||||
|
||||
import RouteErrorBoundary from './RouteErrorBoundary';
|
||||
|
||||
|
@ -13,39 +13,59 @@ const RouteGuard: FC<{
|
|||
page?: string;
|
||||
}> = ({ children, onEnter, path, page }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const loaderData = useLoaderData();
|
||||
const gr = onEnter({
|
||||
loaderData,
|
||||
path,
|
||||
page,
|
||||
const [gk, setKeeper] = useState<TGuardResult>({
|
||||
ok: true,
|
||||
});
|
||||
const [gkError, setGkError] = useState<TGuardResult['error']>();
|
||||
const applyGuard = () => {
|
||||
if (typeof onEnter !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
let guardError;
|
||||
const errCode = gr.error?.code;
|
||||
if (errCode === '403' || errCode === '404' || errCode === '50X') {
|
||||
guardError = {
|
||||
code: errCode,
|
||||
msg: gr.error?.msg,
|
||||
};
|
||||
}
|
||||
const handleGuardRedirect = () => {
|
||||
const redirectUrl = gr.redirect;
|
||||
if (redirectUrl) {
|
||||
floppyNavigation.navigate(redirectUrl, () => {
|
||||
navigate(redirectUrl, { replace: true });
|
||||
const gr = onEnter({
|
||||
loaderData,
|
||||
path,
|
||||
page,
|
||||
});
|
||||
|
||||
setKeeper(gr);
|
||||
if (
|
||||
gk.ok === false &&
|
||||
gk.error?.code &&
|
||||
/403|404|50X/i.test(gk.error.code.toString())
|
||||
) {
|
||||
setGkError(gk.error);
|
||||
return;
|
||||
}
|
||||
setGkError(undefined);
|
||||
if (gr.redirect) {
|
||||
floppyNavigation.navigate(gr.redirect, {
|
||||
handler: navigate,
|
||||
options: { replace: true },
|
||||
});
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
handleGuardRedirect();
|
||||
}, [location]);
|
||||
/**
|
||||
* By detecting changes to location.href, many unnecessary tests can be avoided
|
||||
*/
|
||||
applyGuard();
|
||||
}, [window.location.href]);
|
||||
|
||||
let asOK = gk.ok;
|
||||
if (gk.ok === false && gk.redirect) {
|
||||
/**
|
||||
* It is possible that the route guard verification fails
|
||||
* but the current page is already the target page for the route guard jump
|
||||
* This should render `children`!
|
||||
*/
|
||||
asOK = floppyNavigation.equalToCurrentHref(gk.redirect);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{gr.ok ? children : null}
|
||||
{!gr.ok && guardError ? (
|
||||
<RouteErrorBoundary errCode={guardError.code} />
|
||||
) : null}
|
||||
{asOK ? children : null}
|
||||
{gkError ? <RouteErrorBoundary errCode={gkError.code as string} /> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
export const RouteAlias = {
|
||||
home: '/',
|
||||
login: '/users/login',
|
||||
register: '/users/register',
|
||||
activation: '/users/login?status=inactive',
|
||||
signUp: '/users/register',
|
||||
inactive: '/users/login?status=inactive',
|
||||
accountRecovery: '/users/account-recovery',
|
||||
changeEmail: '/users/change-email',
|
||||
passwordReset: '/users/password-reset',
|
||||
accountActivation: '/users/account-activation',
|
||||
activationSuccess: '/users/account-activation/success',
|
||||
activationFailed: '/users/account-activation/failed',
|
||||
suspended: '/users/account-suspended',
|
||||
confirmNewEmail: '/users/confirm-new-email',
|
||||
confirmEmail: '/users/confirm-email',
|
||||
authLanding: '/users/auth-landing',
|
||||
};
|
||||
|
|
|
@ -169,6 +169,7 @@ const routes: RouteNode[] = [
|
|||
if (notLogged.ok) {
|
||||
return notLogged;
|
||||
}
|
||||
|
||||
return guard.notActivated();
|
||||
},
|
||||
},
|
||||
|
@ -180,7 +181,14 @@ const routes: RouteNode[] = [
|
|||
if (!allowNew.ok) {
|
||||
return allowNew;
|
||||
}
|
||||
return guard.notLogged();
|
||||
const notLogged = guard.notLogged();
|
||||
if (notLogged.ok) {
|
||||
const sa = guard.singUpAgent();
|
||||
if (!sa.ok) {
|
||||
return sa;
|
||||
}
|
||||
}
|
||||
return notLogged;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -232,8 +240,8 @@ const routes: RouteNode[] = [
|
|||
page: 'pages/Users/OauthBindEmail',
|
||||
},
|
||||
{
|
||||
path: '/users/oauth',
|
||||
page: 'pages/Users/OauthCallback',
|
||||
path: '/users/auth-landing',
|
||||
page: 'pages/Users/AuthCallback',
|
||||
},
|
||||
{
|
||||
path: '/posts/:qid/timeline',
|
||||
|
@ -261,7 +269,7 @@ const routes: RouteNode[] = [
|
|||
path: 'admin',
|
||||
page: 'pages/Admin',
|
||||
loader: async () => {
|
||||
await guard.pullLoggedUser(true);
|
||||
await guard.pullLoggedUser();
|
||||
return null;
|
||||
},
|
||||
guard: () => {
|
||||
|
@ -337,7 +345,15 @@ const routes: RouteNode[] = [
|
|||
page: 'pages/Admin/Login',
|
||||
},
|
||||
{
|
||||
path: 'installed_plugins',
|
||||
path: 'settings-users',
|
||||
page: 'pages/Admin/SettingsUsers',
|
||||
},
|
||||
{
|
||||
path: 'privileges',
|
||||
page: 'pages/Admin/Privileges',
|
||||
},
|
||||
{
|
||||
path: 'installed-plugins',
|
||||
page: 'pages/Admin/Plugins/Installed',
|
||||
},
|
||||
{
|
||||
|
@ -346,6 +362,18 @@ const routes: RouteNode[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/user-center/auth',
|
||||
page: 'pages/UserCenter/Auth',
|
||||
guard: () => {
|
||||
const notLogged = guard.notLogged();
|
||||
return notLogged;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/user-center/auth-failed',
|
||||
page: 'pages/UserCenter/AuthFailed',
|
||||
},
|
||||
// for review
|
||||
{
|
||||
path: 'review',
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import qs from 'qs';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import type * as Types from '@/common/interface';
|
||||
import request from '@/utils/request';
|
||||
import type { PluginConfig } from '@/plugins/types';
|
||||
|
||||
export const useQueryPlugins = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;
|
||||
|
@ -24,7 +24,7 @@ export const updatePluginStatus = (params) => {
|
|||
|
||||
export const useQueryPluginConfig = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugin/config?${qs.stringify(params)}`;
|
||||
const { data, error, mutate } = useSWR<Types.PluginConfig, Error>(
|
||||
const { data, error, mutate } = useSWR<PluginConfig, Error>(
|
||||
apiUrl,
|
||||
request.instance.get,
|
||||
);
|
||||
|
|
|
@ -3,6 +3,30 @@ import useSWR from 'swr';
|
|||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export interface AdminSettingsUsers {
|
||||
allow_update_avatar: boolean;
|
||||
allow_update_bio: boolean;
|
||||
allow_update_display_name: boolean;
|
||||
allow_update_location: boolean;
|
||||
allow_update_username: boolean;
|
||||
allow_update_website: boolean;
|
||||
default_avatar: string;
|
||||
}
|
||||
|
||||
interface PrivilegeLevel {
|
||||
level: number;
|
||||
level_desc: string;
|
||||
privileges: {
|
||||
label: string;
|
||||
value: number;
|
||||
key: string;
|
||||
}[];
|
||||
}
|
||||
export interface AdminSettingsPrivilege {
|
||||
selected_level: number;
|
||||
options: PrivilegeLevel[];
|
||||
}
|
||||
|
||||
export const useGeneralSetting = () => {
|
||||
const apiUrl = `/answer/admin/api/siteinfo/general`;
|
||||
const { data, error } = useSWR<Type.AdminSettingsGeneral, Error>(
|
||||
|
@ -126,3 +150,23 @@ export const getLoginSetting = () => {
|
|||
export const putLoginSetting = (params: Type.AdminSettingsLogin) => {
|
||||
return request.put('/answer/admin/api/siteinfo/login', params);
|
||||
};
|
||||
|
||||
export const getUsersSetting = () => {
|
||||
return request.get<AdminSettingsUsers>('/answer/admin/api/siteinfo/users');
|
||||
};
|
||||
|
||||
export const putUsersSetting = (params: AdminSettingsUsers) => {
|
||||
return request.put('/answer/admin/api/siteinfo/users', params);
|
||||
};
|
||||
|
||||
export const getPrivilegeSetting = () => {
|
||||
return request.get<AdminSettingsPrivilege>(
|
||||
'/answer/admin/api/setting/privileges',
|
||||
);
|
||||
};
|
||||
|
||||
export const putPrivilegeSetting = (level: number) => {
|
||||
return request.put('/answer/admin/api/setting/privileges', {
|
||||
level,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ export const useHotQuestions = (
|
|||
page: 1,
|
||||
page_size: 6,
|
||||
order: 'frequent',
|
||||
in_days: 7,
|
||||
},
|
||||
) => {
|
||||
const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`;
|
||||
|
|
|
@ -130,8 +130,8 @@ export const resendEmail = (params?: Type.ImgCodeReq) => {
|
|||
* @description get login userinfo
|
||||
* @returns {UserInfo}
|
||||
*/
|
||||
export const getLoggedUserInfo = () => {
|
||||
return request.get<Type.UserInfoRes>('/answer/api/v1/user/info');
|
||||
export const getLoggedUserInfo = (config = { passingError: false }) => {
|
||||
return request.get<Type.UserInfoRes>('/answer/api/v1/user/info', config);
|
||||
};
|
||||
|
||||
export const modifyPassword = (params: Type.ModifyPasswordReq) => {
|
||||
|
|
|
@ -2,3 +2,4 @@ export * from './admin';
|
|||
export * from './common';
|
||||
export * from './client';
|
||||
export * from './install';
|
||||
export * from './user-center';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue