mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/1.0.7/ui' of github.com:answerdev/answer into feat/1.0.7/ui
This commit is contained in:
commit
8fe292775a
|
@ -270,6 +270,7 @@ ui:
|
|||
tag: Tag
|
||||
tags: Tags
|
||||
tag_wiki: tag wiki
|
||||
create_tag: Create Tag
|
||||
edit_tag: Edit Tag
|
||||
ask_a_question: Add Question
|
||||
edit_question: Edit Question
|
||||
|
@ -433,7 +434,7 @@ ui:
|
|||
range: Display name up to 35 characters.
|
||||
slug_name:
|
||||
label: URL Slug
|
||||
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
desc: URL slug up to 35 characters.
|
||||
msg:
|
||||
empty: URL slug cannot be empty.
|
||||
range: URL slug up to 35 characters.
|
||||
|
@ -442,6 +443,7 @@ ui:
|
|||
label: Description
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_post: Post new tag
|
||||
tag_info:
|
||||
created_at: Created
|
||||
edited_at: Edited
|
||||
|
|
|
@ -24,13 +24,13 @@ export interface ReportParams {
|
|||
export interface TagBase {
|
||||
display_name: string;
|
||||
slug_name: string;
|
||||
recommend: boolean;
|
||||
reserved: boolean;
|
||||
original_text?: string;
|
||||
recommend?: boolean;
|
||||
reserved?: boolean;
|
||||
}
|
||||
|
||||
export interface Tag extends TagBase {
|
||||
main_tag_slug_name?: string;
|
||||
original_text?: string;
|
||||
parsed_text?: string;
|
||||
}
|
||||
|
||||
|
@ -101,6 +101,11 @@ export interface ModifyUserReq {
|
|||
website: string;
|
||||
}
|
||||
|
||||
enum RoleId {
|
||||
User = 1,
|
||||
Admin = 2,
|
||||
Moderator = 3,
|
||||
}
|
||||
export interface UserInfoBase {
|
||||
id?: string;
|
||||
avatar: any;
|
||||
|
@ -114,7 +119,7 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
is_admin?: boolean;
|
||||
role_id: RoleId;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
|
@ -127,7 +132,6 @@ export interface UserInfoRes extends UserInfoBase {
|
|||
*/
|
||||
mail_status: number;
|
||||
language: string;
|
||||
is_admin: boolean;
|
||||
e_mail?: string;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { Icon } from '@/components';
|
||||
import './index.css';
|
||||
|
||||
|
@ -101,10 +102,12 @@ const AccordionNav: FC<AccordionProps> = ({ menus = [], path = '/' }) => {
|
|||
|
||||
const [openKey, setOpenKey] = useState(getOpenKey());
|
||||
const menuClick = (evt, menu, href, isLeaf) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (isLeaf) {
|
||||
navigate(href);
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
navigate(href);
|
||||
}
|
||||
} else {
|
||||
setOpenKey(openKey === menu.name ? '' : menu.name);
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
onClick={handleLinkClick}>
|
||||
{t('header.nav.setting')}
|
||||
</Dropdown.Item>
|
||||
{userInfo?.is_admin ? (
|
||||
{userInfo?.role_id === 2 ? (
|
||||
<Dropdown.Item href="/admin" onClick={handleLinkClick}>
|
||||
{t('header.nav.admin')}
|
||||
</Dropdown.Item>
|
||||
|
|
|
@ -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 { is_admin = false } = loggedUserInfoStore((state) => state.user);
|
||||
const { role_id = 1 } = loggedUserInfoStore((state) => state.user);
|
||||
|
||||
const { tagId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
@ -219,7 +219,7 @@ const Index = () => {
|
|||
<Form.Control
|
||||
value={formData.displayName.value}
|
||||
isInvalid={formData.displayName.isInvalid}
|
||||
disabled={!is_admin}
|
||||
disabled={role_id !== 2}
|
||||
onChange={handleDisplayNameChange}
|
||||
/>
|
||||
|
||||
|
@ -232,7 +232,7 @@ const Index = () => {
|
|||
<Form.Control
|
||||
value={formData.slugName.value}
|
||||
isInvalid={formData.slugName.isInvalid}
|
||||
disabled={!is_admin}
|
||||
disabled={role_id !== 2}
|
||||
onChange={handleSlugNameChange}
|
||||
/>
|
||||
<Form.Text as="div">{t('form.fields.slug_name.info')}</Form.Text>
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { useState } from 'react';
|
||||
import { Container, Row, Col, Card, Button, Form } from 'react-bootstrap';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Button,
|
||||
Form,
|
||||
Stack,
|
||||
} from 'react-bootstrap';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
@ -8,6 +16,7 @@ import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components';
|
|||
import { formatCount } from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import { useQueryTags, following } from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const sortBtns = ['popular', 'name', 'newest'];
|
||||
|
||||
|
@ -15,6 +24,7 @@ const Tags = () => {
|
|||
const [urlSearch] = useSearchParams();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
|
||||
const [searchTag, setSearchTag] = useState('');
|
||||
const { role_id } = loggedUserInfoStore((_) => _.user);
|
||||
|
||||
const page = Number(urlSearch.get('page')) || 1;
|
||||
const sort = urlSearch.get('sort');
|
||||
|
@ -55,17 +65,26 @@ const Tags = () => {
|
|||
<Col xxl={10} sm={12}>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
value={searchTag}
|
||||
placeholder={t('search_placeholder')}
|
||||
type="text"
|
||||
onChange={handleChange}
|
||||
size="sm"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
value={searchTag}
|
||||
placeholder={t('search_placeholder')}
|
||||
type="text"
|
||||
onChange={handleChange}
|
||||
size="sm"
|
||||
/>
|
||||
</Form.Group>
|
||||
</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
|
||||
data={sortBtns}
|
||||
currentSort={sort || 'popular'}
|
||||
|
|
|
@ -15,7 +15,7 @@ import HistoryItem from './components/Item';
|
|||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||
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 [isLoading, setLoading] = useState(false);
|
||||
const [timelineData, setTimelineData] = useState<Type.TimelineRes>();
|
||||
|
@ -114,7 +114,7 @@ const Index: FC = () => {
|
|||
data={item}
|
||||
objectInfo={timelineData?.object_info}
|
||||
key={item.activity_id}
|
||||
isAdmin={is_admin}
|
||||
isAdmin={role_id === 2}
|
||||
revisionList={revisionList}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -37,7 +37,7 @@ const Index: FC<Props> = ({ data }) => {
|
|||
) : (
|
||||
<span className="link-dark h3 mb-0">{data.display_name}</span>
|
||||
)}
|
||||
{data?.is_admin && (
|
||||
{data?.role_id === 2 && (
|
||||
<div className="ms-2">
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
|
|
|
@ -80,6 +80,13 @@ const routes: RouteNode[] = [
|
|||
path: 'tags',
|
||||
page: 'pages/Tags',
|
||||
},
|
||||
{
|
||||
path: 'tags/create',
|
||||
page: 'pages/Tags/Create',
|
||||
guard: () => {
|
||||
return guard.isAdminOrModerator();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tags/:tagName',
|
||||
page: 'pages/Tags/Detail',
|
||||
|
|
|
@ -63,3 +63,8 @@ export const getTagsBySlugName = (slugNames: string) => {
|
|||
const apiUrl = `/answer/api/v1/tags?tags=${encodeURIComponent(slugNames)}`;
|
||||
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);
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ const initUser: UserInfoRes = {
|
|||
status: '',
|
||||
mail_status: 1,
|
||||
language: 'Default',
|
||||
is_admin: false,
|
||||
role_id: 1,
|
||||
};
|
||||
|
||||
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 = {
|
||||
differentCurrent,
|
||||
navigate,
|
||||
navigateToLogin,
|
||||
shouldProcessLinkClick,
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@ type TLoginState = {
|
|||
isForbidden: boolean;
|
||||
isNormal: boolean;
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
};
|
||||
|
||||
export type TGuardResult = {
|
||||
|
@ -40,6 +41,7 @@ export const deriveLoginState = (): TLoginState => {
|
|||
isForbidden: false,
|
||||
isNormal: false,
|
||||
isAdmin: false,
|
||||
isModerator: false,
|
||||
};
|
||||
const { user } = loggedUserInfoStore.getState();
|
||||
if (user.access_token) {
|
||||
|
@ -57,9 +59,12 @@ export const deriveLoginState = (): TLoginState => {
|
|||
if (ls.isActivated && !ls.isForbidden) {
|
||||
ls.isNormal = true;
|
||||
}
|
||||
if (ls.isNormal && user.is_admin === true) {
|
||||
if (ls.isNormal && user.role_id === 2) {
|
||||
ls.isAdmin = true;
|
||||
}
|
||||
if (ls.isNormal && user.role_id === 3) {
|
||||
ls.isModerator = true;
|
||||
}
|
||||
return ls;
|
||||
};
|
||||
|
||||
|
@ -174,6 +179,16 @@ export const admin = () => {
|
|||
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 = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const loginSetting = loginSettingStore.getState().login;
|
||||
|
|
Loading…
Reference in New Issue