mirror of https://gitee.com/answerdev/answer.git
commit
4c1db03a44
|
@ -278,6 +278,7 @@ ui:
|
||||||
tag: Tag
|
tag: Tag
|
||||||
tags: Tags
|
tags: Tags
|
||||||
tag_wiki: tag wiki
|
tag_wiki: tag wiki
|
||||||
|
create_tag: Create Tag
|
||||||
edit_tag: Edit Tag
|
edit_tag: Edit Tag
|
||||||
ask_a_question: Add Question
|
ask_a_question: Add Question
|
||||||
edit_question: Edit Question
|
edit_question: Edit Question
|
||||||
|
@ -441,7 +442,7 @@ ui:
|
||||||
range: Display name up to 35 characters.
|
range: Display name up to 35 characters.
|
||||||
slug_name:
|
slug_name:
|
||||||
label: URL Slug
|
label: URL Slug
|
||||||
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
desc: URL slug up to 35 characters.
|
||||||
msg:
|
msg:
|
||||||
empty: URL slug cannot be empty.
|
empty: URL slug cannot be empty.
|
||||||
range: URL slug up to 35 characters.
|
range: URL slug up to 35 characters.
|
||||||
|
@ -450,6 +451,7 @@ ui:
|
||||||
label: Description
|
label: Description
|
||||||
btn_cancel: Cancel
|
btn_cancel: Cancel
|
||||||
btn_submit: Submit
|
btn_submit: Submit
|
||||||
|
btn_post: Post new tag
|
||||||
tag_info:
|
tag_info:
|
||||||
created_at: Created
|
created_at: Created
|
||||||
edited_at: Edited
|
edited_at: Edited
|
||||||
|
@ -1246,6 +1248,9 @@ ui:
|
||||||
label: Timezone
|
label: Timezone
|
||||||
msg: Timezone cannot be empty.
|
msg: Timezone cannot be empty.
|
||||||
text: Choose a city in the same timezone as you.
|
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:
|
smtp:
|
||||||
page_title: SMTP
|
page_title: SMTP
|
||||||
from_email:
|
from_email:
|
||||||
|
|
|
@ -595,3 +595,14 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||||
'reopened',
|
'reopened',
|
||||||
'closed',
|
'closed',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const SYSTEM_AVATAR_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: 'System',
|
||||||
|
value: 'system',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Gravatar',
|
||||||
|
value: 'gravatar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -24,13 +24,13 @@ export interface ReportParams {
|
||||||
export interface TagBase {
|
export interface TagBase {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
slug_name: string;
|
slug_name: string;
|
||||||
recommend: boolean;
|
original_text?: string;
|
||||||
reserved: boolean;
|
recommend?: boolean;
|
||||||
|
reserved?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tag extends TagBase {
|
export interface Tag extends TagBase {
|
||||||
main_tag_slug_name?: string;
|
main_tag_slug_name?: string;
|
||||||
original_text?: string;
|
|
||||||
parsed_text?: string;
|
parsed_text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,11 @@ export interface ModifyUserReq {
|
||||||
website: string;
|
website: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RoleId {
|
||||||
|
User = 1,
|
||||||
|
Admin = 2,
|
||||||
|
Moderator = 3,
|
||||||
|
}
|
||||||
export interface UserInfoBase {
|
export interface UserInfoBase {
|
||||||
id?: string;
|
id?: string;
|
||||||
avatar: any;
|
avatar: any;
|
||||||
|
@ -114,7 +119,7 @@ export interface UserInfoBase {
|
||||||
*/
|
*/
|
||||||
status?: string;
|
status?: string;
|
||||||
/** roles */
|
/** roles */
|
||||||
is_admin?: boolean;
|
role_id: RoleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfoRes extends UserInfoBase {
|
export interface UserInfoRes extends UserInfoBase {
|
||||||
|
@ -127,7 +132,6 @@ export interface UserInfoRes extends UserInfoBase {
|
||||||
*/
|
*/
|
||||||
mail_status: number;
|
mail_status: number;
|
||||||
language: string;
|
language: string;
|
||||||
is_admin: boolean;
|
|
||||||
e_mail?: string;
|
e_mail?: string;
|
||||||
[prop: string]: any;
|
[prop: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -297,6 +301,7 @@ export interface HelmetUpdate extends Omit<HelmetBase, 'pageTitle'> {
|
||||||
export interface AdminSettingsInterface {
|
export interface AdminSettingsInterface {
|
||||||
language: string;
|
language: string;
|
||||||
time_zone?: string;
|
time_zone?: string;
|
||||||
|
default_avatar?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminSettingsSmtp {
|
export interface AdminSettingsSmtp {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
import { Icon } from '@/components';
|
import { Icon } from '@/components';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
@ -101,10 +102,12 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
||||||
|
|
||||||
const [openKey, setOpenKey] = useState(getOpenKey());
|
const [openKey, setOpenKey] = useState(getOpenKey());
|
||||||
const menuClick = (evt, menu, href, isLeaf) => {
|
const menuClick = (evt, menu, href, isLeaf) => {
|
||||||
evt.preventDefault();
|
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||||
|
evt.preventDefault();
|
||||||
navigate(href);
|
navigate(href);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setOpenKey(openKey === menu.name ? '' : menu.name);
|
setOpenKey(openKey === menu.name ? '' : menu.name);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { NavLink, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import type * as Type from '@/common/interface';
|
import type * as Type from '@/common/interface';
|
||||||
import { Avatar, Icon } from '@/components';
|
import { Avatar, Icon } from '@/components';
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redDot: Type.NotificationStatus | undefined;
|
redDot: Type.NotificationStatus | undefined;
|
||||||
|
@ -16,10 +17,12 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleLinkClick = (evt) => {
|
const handleLinkClick = (evt) => {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { href } = evt.currentTarget;
|
const { href } = evt.currentTarget;
|
||||||
const { pathname } = new URL(href);
|
const { pathname } = new URL(href);
|
||||||
navigate(pathname);
|
navigate(pathname);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -63,7 +66,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||||
onClick={handleLinkClick}>
|
onClick={handleLinkClick}>
|
||||||
{t('header.nav.setting')}
|
{t('header.nav.setting')}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
{userInfo?.is_admin ? (
|
{userInfo?.role_id === 2 ? (
|
||||||
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
||||||
{t('header.nav.admin')}
|
{t('header.nav.admin')}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
|
|
@ -70,14 +70,17 @@ const Header: FC = () => {
|
||||||
window.location.replace(window.location.href);
|
window.location.replace(window.location.href);
|
||||||
};
|
};
|
||||||
const onLoginClick = (evt) => {
|
const onLoginClick = (evt) => {
|
||||||
evt.preventDefault();
|
|
||||||
if (location.pathname === '/users/login') {
|
if (location.pathname === '/users/login') {
|
||||||
|
evt.preventDefault();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||||
|
evt.preventDefault();
|
||||||
floppyNavigation.navigateToLogin((loginPath) => {
|
floppyNavigation.navigateToLogin((loginPath) => {
|
||||||
navigate(loginPath, { replace: true });
|
navigate(loginPath, { replace: true });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
reopenQuestion,
|
reopenQuestion,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
import { tryNormalLogged } from '@/utils/guard';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: 'answer' | 'question';
|
type: 'answer' | 'question';
|
||||||
|
@ -109,6 +110,9 @@ const Index: FC<IProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleEdit = (evt, targetUrl) => {
|
const handleEdit = (evt, targetUrl) => {
|
||||||
|
if (!floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
let checkObjectId = qid;
|
let checkObjectId = qid;
|
||||||
if (type === 'answer') {
|
if (type === 'answer') {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Pagination } from 'react-bootstrap';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { scrollToDocTop } from '@/utils';
|
import { scrollToDocTop, floppyNavigation } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
|
@ -48,10 +48,12 @@ const PageItem = ({ page, currentPage, path }: PageItemProps) => {
|
||||||
active={currentPage === page}
|
active={currentPage === page}
|
||||||
href={path}
|
href={path}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(path);
|
navigate(path);
|
||||||
scrollToDocTop();
|
scrollToDocTop();
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
{page}
|
{page}
|
||||||
</Pagination.Item>
|
</Pagination.Item>
|
||||||
|
@ -91,9 +93,11 @@ const Index: FC<Props> = ({
|
||||||
<Pagination.Prev
|
<Pagination.Prev
|
||||||
href={handleParams(currentPage - 1)}
|
href={handleParams(currentPage - 1)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(handleParams(currentPage - 1));
|
navigate(handleParams(currentPage - 1));
|
||||||
scrollToDocTop();
|
scrollToDocTop();
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
{t('prev')}
|
{t('prev')}
|
||||||
</Pagination.Prev>
|
</Pagination.Prev>
|
||||||
|
@ -186,9 +190,11 @@ const Index: FC<Props> = ({
|
||||||
disabled={currentPage === totalPage}
|
disabled={currentPage === totalPage}
|
||||||
href={handleParams(currentPage + 1)}
|
href={handleParams(currentPage + 1)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate(handleParams(currentPage + 1));
|
navigate(handleParams(currentPage + 1));
|
||||||
scrollToDocTop();
|
scrollToDocTop();
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
{t('next')}
|
{t('next')}
|
||||||
</Pagination.Next>
|
</Pagination.Next>
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data;
|
data;
|
||||||
i18nKeyPrefix: string;
|
i18nKeyPrefix: string;
|
||||||
|
@ -39,10 +41,12 @@ const Index: FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e, type) => {
|
const handleClick = (e, type) => {
|
||||||
e.preventDefault();
|
|
||||||
const str = handleParams(type);
|
const str = handleParams(type);
|
||||||
if (pathname) {
|
if (pathname) {
|
||||||
|
if (floppyNavigation.shouldProcessLinkClick(e)) {
|
||||||
|
e.preventDefault();
|
||||||
navigate(`${pathname}${str}`);
|
navigate(`${pathname}${str}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setUrlSearchParams(str);
|
setUrlSearchParams(str);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,14 @@ import {
|
||||||
FormDataType,
|
FormDataType,
|
||||||
AdminSettingsInterface,
|
AdminSettingsInterface,
|
||||||
} from '@/common/interface';
|
} from '@/common/interface';
|
||||||
import { interfaceStore } from '@/stores';
|
import { interfaceStore, loggedUserInfoStore } from '@/stores';
|
||||||
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
import { JSONSchema, SchemaForm, UISchema } from '@/components';
|
||||||
import { DEFAULT_TIMEZONE } from '@/common/constants';
|
import { DEFAULT_TIMEZONE, SYSTEM_AVATAR_OPTIONS } from '@/common/constants';
|
||||||
import { updateInterfaceSetting, useInterfaceSetting } from '@/services';
|
import {
|
||||||
|
updateInterfaceSetting,
|
||||||
|
useInterfaceSetting,
|
||||||
|
getLoggedUserInfo,
|
||||||
|
} from '@/services';
|
||||||
import {
|
import {
|
||||||
setupAppLanguage,
|
setupAppLanguage,
|
||||||
loadLanguageOptions,
|
loadLanguageOptions,
|
||||||
|
@ -42,6 +46,13 @@ const Interface: FC = () => {
|
||||||
title: t('time_zone.label'),
|
title: t('time_zone.label'),
|
||||||
description: t('time_zone.text'),
|
description: t('time_zone.text'),
|
||||||
},
|
},
|
||||||
|
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),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,6 +67,11 @@ const Interface: FC = () => {
|
||||||
isInvalid: false,
|
isInvalid: false,
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
},
|
},
|
||||||
|
default_avatar: {
|
||||||
|
value: setting?.default_avatar || 'System',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const uiSchema: UISchema = {
|
const uiSchema: UISchema = {
|
||||||
|
@ -65,6 +81,9 @@ const Interface: FC = () => {
|
||||||
time_zone: {
|
time_zone: {
|
||||||
'ui:widget': 'timezone',
|
'ui:widget': 'timezone',
|
||||||
},
|
},
|
||||||
|
default_avatar: {
|
||||||
|
'ui:widget': 'select',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
const getLangs = async () => {
|
const getLangs = async () => {
|
||||||
const res: LangsType[] = await loadLanguageOptions(true);
|
const res: LangsType[] = await loadLanguageOptions(true);
|
||||||
|
@ -97,6 +116,7 @@ const Interface: FC = () => {
|
||||||
const reqParams: AdminSettingsInterface = {
|
const reqParams: AdminSettingsInterface = {
|
||||||
language: formData.language.value,
|
language: formData.language.value,
|
||||||
time_zone: formData.time_zone.value,
|
time_zone: formData.time_zone.value,
|
||||||
|
default_avatar: formData.default_avatar.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateInterfaceSetting(reqParams)
|
updateInterfaceSetting(reqParams)
|
||||||
|
@ -104,6 +124,9 @@ const Interface: FC = () => {
|
||||||
interfaceStore.getState().update(reqParams);
|
interfaceStore.getState().update(reqParams);
|
||||||
setupAppLanguage();
|
setupAppLanguage();
|
||||||
setupAppTimeZone();
|
setupAppTimeZone();
|
||||||
|
getLoggedUserInfo().then((info) => {
|
||||||
|
loggedUserInfoStore.getState().update(info);
|
||||||
|
});
|
||||||
Toast.onShow({
|
Toast.onShow({
|
||||||
msg: t('update', { keyPrefix: 'toast' }),
|
msg: t('update', { keyPrefix: 'toast' }),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { usePageTags, usePromptWithUnload } from '@/hooks';
|
||||||
|
import { Editor, EditorRef } from '@/components';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
import type * as Type from '@/common/interface';
|
||||||
|
import { createTag } from '@/services';
|
||||||
|
import { handleFormError } from '@/utils';
|
||||||
|
|
||||||
|
interface FormDataItem {
|
||||||
|
displayName: Type.FormValue<string>;
|
||||||
|
slugName: Type.FormValue<string>;
|
||||||
|
description: Type.FormValue<string>;
|
||||||
|
}
|
||||||
|
const initFormData = {
|
||||||
|
displayName: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
slugName: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
value: '',
|
||||||
|
isInvalid: false,
|
||||||
|
errorMsg: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
const { role_id = 1 } = loggedUserInfoStore((state) => state.user);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation('translation', { keyPrefix: 'tag_modal' });
|
||||||
|
const [focusType, setForceType] = useState('');
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||||
|
const [immData] = useState(initFormData);
|
||||||
|
const [contentChanged, setContentChanged] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = useRef<EditorRef>({
|
||||||
|
getHtml: () => '',
|
||||||
|
});
|
||||||
|
|
||||||
|
usePromptWithUnload({
|
||||||
|
when: contentChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { displayName, slugName, description } = formData;
|
||||||
|
const {
|
||||||
|
displayName: display_name,
|
||||||
|
slugName: slug_name,
|
||||||
|
description: original_text,
|
||||||
|
} = immData;
|
||||||
|
if (!display_name || !slug_name || !original_text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
display_name.value !== displayName.value ||
|
||||||
|
slug_name.value !== slugName.value ||
|
||||||
|
original_text.value !== description.value
|
||||||
|
) {
|
||||||
|
setContentChanged(true);
|
||||||
|
} else {
|
||||||
|
setContentChanged(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
formData.displayName.value,
|
||||||
|
formData.slugName.value,
|
||||||
|
formData.description.value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleDescriptionChange = (value: string) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
description: { ...formData.description, value },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setContentChanged(false);
|
||||||
|
const params = {
|
||||||
|
display_name: formData.displayName.value,
|
||||||
|
slug_name: formData.slugName.value,
|
||||||
|
original_text: formData.description.value,
|
||||||
|
};
|
||||||
|
createTag(params)
|
||||||
|
.then((res) => {
|
||||||
|
navigate(`/tags/${res.slug_name}/info`, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.isError) {
|
||||||
|
const data = handleFormError(err, formData, [
|
||||||
|
{ from: 'display_name', to: 'displayName' },
|
||||||
|
{ from: 'slug_name', to: 'slugName' },
|
||||||
|
{ from: 'original_text', to: 'description' },
|
||||||
|
]);
|
||||||
|
setFormData({ ...data });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisplayNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
displayName: { ...formData.displayName, value: e.currentTarget.value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSlugNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
slugName: { ...formData.slugName, value: e.currentTarget.value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
usePageTags({
|
||||||
|
title: t('create_tag', { keyPrefix: 'page_title' }),
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Container className="pt-4 mt-2 mb-5 edit-answer-wrap">
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col xxl={10} md={12}>
|
||||||
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className="justify-content-center">
|
||||||
|
<Col xxl={7} lg={8} sm={12} className="mb-4 mb-md-0">
|
||||||
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
|
<Form.Group controlId="display_name" className="mb-3">
|
||||||
|
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.displayName.value}
|
||||||
|
isInvalid={formData.displayName.isInvalid}
|
||||||
|
disabled={role_id !== 2}
|
||||||
|
onChange={handleDisplayNameChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.displayName.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="slug_name" className="mb-3">
|
||||||
|
<Form.Label>{t('form.fields.slug_name.label')}</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.slugName.value}
|
||||||
|
isInvalid={formData.slugName.isInvalid}
|
||||||
|
disabled={role_id !== 2}
|
||||||
|
onChange={handleSlugNameChange}
|
||||||
|
/>
|
||||||
|
<Form.Text as="div">{t('form.fields.slug_name.desc')}</Form.Text>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.slugName.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="description" className="mt-4">
|
||||||
|
<Form.Label>{t('form.fields.desc.label')}</Form.Label>
|
||||||
|
<Editor
|
||||||
|
value={formData.description.value}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
className={classNames(
|
||||||
|
'form-control p-0',
|
||||||
|
focusType === 'description' && 'focus',
|
||||||
|
)}
|
||||||
|
onFocus={() => {
|
||||||
|
setForceType('description');
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setForceType('');
|
||||||
|
}}
|
||||||
|
ref={editorRef}
|
||||||
|
/>
|
||||||
|
<Form.Control
|
||||||
|
value={formData.description.value}
|
||||||
|
type="text"
|
||||||
|
isInvalid={formData.description.isInvalid}
|
||||||
|
readOnly
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formData.description.errorMsg}
|
||||||
|
</Form.Control.Feedback>
|
||||||
|
</Form.Group>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button type="submit">{t('btn_post')}</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Col>
|
||||||
|
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||||
|
<Card>
|
||||||
|
<Card.Header>
|
||||||
|
{t('title', { keyPrefix: 'how_to_format' })}
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Body
|
||||||
|
className="fmt small"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t('desc', { keyPrefix: 'how_to_format' }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
|
@ -42,7 +42,7 @@ const initFormData = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
const { role_id = 1 } = loggedUserInfoStore((state) => state.user);
|
||||||
|
|
||||||
const { tagId } = useParams();
|
const { tagId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -219,7 +219,7 @@ const Index = () => {
|
||||||
<Form.Control
|
<Form.Control
|
||||||
value={formData.displayName.value}
|
value={formData.displayName.value}
|
||||||
isInvalid={formData.displayName.isInvalid}
|
isInvalid={formData.displayName.isInvalid}
|
||||||
disabled={!is_admin}
|
disabled={role_id !== 2}
|
||||||
onChange={handleDisplayNameChange}
|
onChange={handleDisplayNameChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ const Index = () => {
|
||||||
<Form.Control
|
<Form.Control
|
||||||
value={formData.slugName.value}
|
value={formData.slugName.value}
|
||||||
isInvalid={formData.slugName.isInvalid}
|
isInvalid={formData.slugName.isInvalid}
|
||||||
disabled={!is_admin}
|
disabled={role_id !== 2}
|
||||||
onChange={handleSlugNameChange}
|
onChange={handleSlugNameChange}
|
||||||
/>
|
/>
|
||||||
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
|
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
import {
|
||||||
import { useSearchParams } from 'react-router-dom';
|
Container,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Stack,
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
import { useSearchParams, Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { usePageTags } from '@/hooks';
|
import { usePageTags } from '@/hooks';
|
||||||
|
@ -8,6 +16,7 @@ import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
||||||
import { formatCount } from '@/utils';
|
import { formatCount } from '@/utils';
|
||||||
import { tryNormalLogged } from '@/utils/guard';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import { useQueryTags, following } from '@/services';
|
import { useQueryTags, following } from '@/services';
|
||||||
|
import { loggedUserInfoStore } from '@/stores';
|
||||||
|
|
||||||
const sortBtns = ['popular', 'name', 'newest'];
|
const sortBtns = ['popular', 'name', 'newest'];
|
||||||
|
|
||||||
|
@ -15,6 +24,7 @@ const Tags = () => {
|
||||||
const [urlSearch] = useSearchParams();
|
const [urlSearch] = useSearchParams();
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
|
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
|
||||||
const [searchTag, setSearchTag] = useState('');
|
const [searchTag, setSearchTag] = useState('');
|
||||||
|
const { role_id } = loggedUserInfoStore((_) => _.user);
|
||||||
|
|
||||||
const page = Number(urlSearch.get('page')) || 1;
|
const page = Number(urlSearch.get('page')) || 1;
|
||||||
const sort = urlSearch.get('sort');
|
const sort = urlSearch.get('sort');
|
||||||
|
@ -55,6 +65,7 @@ const Tags = () => {
|
||||||
<Col xxl={10} sm={12}>
|
<Col xxl={10} sm={12}>
|
||||||
<h3 className="mb-4">{t('title')}</h3>
|
<h3 className="mb-4">{t('title')}</h3>
|
||||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||||
|
<Stack direction="horizontal" gap={3}>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="formBasicEmail">
|
<Form.Group controlId="formBasicEmail">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
@ -66,6 +77,14 @@ const Tags = () => {
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
|
{role_id === 2 || role_id === 3 ? (
|
||||||
|
<Link
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
to="/tags/create">
|
||||||
|
{t('title', { keyPrefix: 'tag_modal' })}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
<QueryGroup
|
<QueryGroup
|
||||||
data={sortBtns}
|
data={sortBtns}
|
||||||
currentSort={sort || 'popular'}
|
currentSort={sort || 'popular'}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import HistoryItem from './components/Item';
|
||||||
const Index: FC = () => {
|
const Index: FC = () => {
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||||
const { qid = '', aid = '', tid = '' } = useParams();
|
const { qid = '', aid = '', tid = '' } = useParams();
|
||||||
const { is_admin } = loggedUserInfoStore((state) => state.user);
|
const { role_id } = loggedUserInfoStore((state) => state.user);
|
||||||
const [showVotes, setShowVotes] = useState(false);
|
const [showVotes, setShowVotes] = useState(false);
|
||||||
const [isLoading, setLoading] = useState(false);
|
const [isLoading, setLoading] = useState(false);
|
||||||
const [timelineData, setTimelineData] = useState<Type.TimelineRes>();
|
const [timelineData, setTimelineData] = useState<Type.TimelineRes>();
|
||||||
|
@ -114,7 +114,7 @@ const Index: FC = () => {
|
||||||
data={item}
|
data={item}
|
||||||
objectInfo={timelineData?.object_info}
|
objectInfo={timelineData?.object_info}
|
||||||
key={item.activity_id}
|
key={item.activity_id}
|
||||||
isAdmin={is_admin}
|
isAdmin={role_id === 2}
|
||||||
revisionList={revisionList}
|
revisionList={revisionList}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
clearNotificationStatus,
|
clearNotificationStatus,
|
||||||
readNotification,
|
readNotification,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
|
import { floppyNavigation } from '@/utils';
|
||||||
|
|
||||||
import Inbox from './components/Inbox';
|
import Inbox from './components/Inbox';
|
||||||
import Achievements from './components/Achievements';
|
import Achievements from './components/Achievements';
|
||||||
|
@ -45,6 +46,9 @@ const Notifications = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleTypeChange = (evt, val) => {
|
const handleTypeChange = (evt, val) => {
|
||||||
|
if (!floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (type === val) {
|
if (type === val) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -37,7 +37,7 @@ const Index: FC<Props> = ({ data }) => {
|
||||||
) : (
|
) : (
|
||||||
<span className="link-dark h3 mb-0">{data.display_name}</span>
|
<span className="link-dark h3 mb-0">{data.display_name}</span>
|
||||||
)}
|
)}
|
||||||
{data?.is_admin && (
|
{data?.role_id === 2 && (
|
||||||
<div className="ms-2">
|
<div className="ms-2">
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="top"
|
placement="top"
|
||||||
|
|
|
@ -80,6 +80,13 @@ const routes: RouteNode[] = [
|
||||||
path: 'tags',
|
path: 'tags',
|
||||||
page: 'pages/Tags',
|
page: 'pages/Tags',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'tags/create',
|
||||||
|
page: 'pages/Tags/Create',
|
||||||
|
guard: () => {
|
||||||
|
return guard.isAdminOrModerator();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'tags/:tagName',
|
path: 'tags/:tagName',
|
||||||
page: 'pages/Tags/Detail',
|
page: 'pages/Tags/Detail',
|
||||||
|
|
|
@ -65,3 +65,8 @@ export const getTagsBySlugName = (slugNames: string) => {
|
||||||
const apiUrl = `/answer/api/v1/tags?tags=${encodeURIComponent(slugNames)}`;
|
const apiUrl = `/answer/api/v1/tags?tags=${encodeURIComponent(slugNames)}`;
|
||||||
return request.get<Type.TagInfo[]>(apiUrl);
|
return request.get<Type.TagInfo[]>(apiUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createTag = (params: Type.TagBase) => {
|
||||||
|
const apiUrl = '/answer/api/v1/tag';
|
||||||
|
return request.post<Type.TagInfo>(apiUrl, params);
|
||||||
|
};
|
||||||
|
|
|
@ -12,6 +12,7 @@ const interfaceSetting = create<InterfaceType>((set) => ({
|
||||||
interface: {
|
interface: {
|
||||||
language: DEFAULT_LANG,
|
language: DEFAULT_LANG,
|
||||||
time_zone: '',
|
time_zone: '',
|
||||||
|
default_avatar: 'system',
|
||||||
},
|
},
|
||||||
update: (params) =>
|
update: (params) =>
|
||||||
set(() => {
|
set(() => {
|
||||||
|
|
|
@ -26,7 +26,7 @@ const initUser: UserInfoRes = {
|
||||||
status: '',
|
status: '',
|
||||||
mail_status: 1,
|
mail_status: 1,
|
||||||
language: 'Default',
|
language: 'Default',
|
||||||
is_admin: false,
|
role_id: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||||
|
|
|
@ -43,8 +43,27 @@ const navigateToLogin = (callback?: Function) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if a Link click event should be handled
|
||||||
|
*/
|
||||||
|
const shouldProcessLinkClick = (evt) => {
|
||||||
|
if (evt.defaultPrevented) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { target, nodeName } = evt.currentTarget;
|
||||||
|
if (nodeName.toLowerCase() !== 'a') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
evt.button === 0 &&
|
||||||
|
(!target || target === '_self') &&
|
||||||
|
!(evt.metaKey || evt.ctrlKey || evt.shiftKey || evt.altKey)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const floppyNavigation = {
|
export const floppyNavigation = {
|
||||||
differentCurrent,
|
differentCurrent,
|
||||||
navigate,
|
navigate,
|
||||||
navigateToLogin,
|
navigateToLogin,
|
||||||
|
shouldProcessLinkClick,
|
||||||
};
|
};
|
||||||
|
|
|
@ -24,6 +24,7 @@ type TLoginState = {
|
||||||
isForbidden: boolean;
|
isForbidden: boolean;
|
||||||
isNormal: boolean;
|
isNormal: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
isModerator: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGuardResult = {
|
export type TGuardResult = {
|
||||||
|
@ -40,6 +41,7 @@ export const deriveLoginState = (): TLoginState => {
|
||||||
isForbidden: false,
|
isForbidden: false,
|
||||||
isNormal: false,
|
isNormal: false,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
|
isModerator: false,
|
||||||
};
|
};
|
||||||
const { user } = loggedUserInfoStore.getState();
|
const { user } = loggedUserInfoStore.getState();
|
||||||
if (user.access_token) {
|
if (user.access_token) {
|
||||||
|
@ -57,9 +59,12 @@ export const deriveLoginState = (): TLoginState => {
|
||||||
if (ls.isActivated && !ls.isForbidden) {
|
if (ls.isActivated && !ls.isForbidden) {
|
||||||
ls.isNormal = true;
|
ls.isNormal = true;
|
||||||
}
|
}
|
||||||
if (ls.isNormal && user.is_admin === true) {
|
if (ls.isNormal && user.role_id === 2) {
|
||||||
ls.isAdmin = true;
|
ls.isAdmin = true;
|
||||||
}
|
}
|
||||||
|
if (ls.isNormal && user.role_id === 3) {
|
||||||
|
ls.isModerator = true;
|
||||||
|
}
|
||||||
return ls;
|
return ls;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -174,6 +179,16 @@ export const admin = () => {
|
||||||
return gr;
|
return gr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAdminOrModerator = () => {
|
||||||
|
const gr = logged();
|
||||||
|
const us = deriveLoginState();
|
||||||
|
if (gr.ok && !us.isAdmin && !us.isModerator) {
|
||||||
|
gr.ok = false;
|
||||||
|
gr.redirect = RouteAlias.home;
|
||||||
|
}
|
||||||
|
return gr;
|
||||||
|
};
|
||||||
|
|
||||||
export const allowNewRegistration = () => {
|
export const allowNewRegistration = () => {
|
||||||
const gr: TGuardResult = { ok: true };
|
const gr: TGuardResult = { ok: true };
|
||||||
const loginSetting = loginSettingStore.getState().login;
|
const loginSetting = loginSettingStore.getState().login;
|
||||||
|
|
Loading…
Reference in New Issue