Merge branch 'test' of github.com:answerdev/answer into test
This commit is contained in:
@@ -1183,14 +1183,6 @@ ui:
|
||||
accepted: Accepted
|
||||
answered: answered
|
||||
asked: asked
|
||||
rank_type:
|
||||
upvote: upvote
|
||||
upvoted: upvoted
|
||||
downvote: downvote
|
||||
downvoted: downvoted
|
||||
edit: edit
|
||||
accept: accept
|
||||
accepted: accepted
|
||||
downvoted: downvoted
|
||||
mod_short: Mod
|
||||
mod_long: Moderators
|
||||
|
||||
@@ -17,12 +17,12 @@ import (
|
||||
|
||||
type AvatarMiddleware struct {
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
uploaderService *uploader.UploaderService
|
||||
uploaderService uploader.UploaderService
|
||||
}
|
||||
|
||||
// NewAvatarMiddleware new auth user middleware
|
||||
func NewAvatarMiddleware(serviceConfig *service_config.ServiceConfig,
|
||||
uploaderService *uploader.UploaderService,
|
||||
uploaderService uploader.UploaderService,
|
||||
) *AvatarMiddleware {
|
||||
return &AvatarMiddleware{
|
||||
serviceConfig: serviceConfig,
|
||||
|
||||
@@ -21,11 +21,11 @@ const (
|
||||
|
||||
// UploadController upload controller
|
||||
type UploadController struct {
|
||||
uploaderService *uploader.UploaderService
|
||||
uploaderService uploader.UploaderService
|
||||
}
|
||||
|
||||
// NewUploadController new controller
|
||||
func NewUploadController(uploaderService *uploader.UploaderService) *UploadController {
|
||||
func NewUploadController(uploaderService uploader.UploaderService) *UploadController {
|
||||
return &UploadController{
|
||||
uploaderService: uploaderService,
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type UserController struct {
|
||||
userService *service.UserService
|
||||
authService *auth.AuthService
|
||||
actionService *action.CaptchaService
|
||||
uploaderService *uploader.UploaderService
|
||||
uploaderService uploader.UploaderService
|
||||
emailService *export.EmailService
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func NewUserController(
|
||||
userService *service.UserService,
|
||||
actionService *action.CaptchaService,
|
||||
emailService *export.EmailService,
|
||||
uploaderService *uploader.UploaderService,
|
||||
uploaderService uploader.UploaderService,
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
|
||||
) *UserController {
|
||||
return &UserController{
|
||||
|
||||
@@ -51,29 +51,36 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// UploaderService user service
|
||||
type UploaderService struct {
|
||||
type UploaderService interface {
|
||||
UploadAvatarFile(ctx *gin.Context) (url string, err error)
|
||||
AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (avatarFile []byte, err error)
|
||||
UploadPostFile(ctx *gin.Context) (url string, err error)
|
||||
UploadBrandingFile(ctx *gin.Context) (url string, err error)
|
||||
}
|
||||
|
||||
// uploaderService uploader service
|
||||
type uploaderService struct {
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewUploaderService new upload service
|
||||
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService) UploaderService {
|
||||
for _, subPath := range subPathList {
|
||||
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return &UploaderService{
|
||||
return &uploaderService{
|
||||
serviceConfig: serviceConfig,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadAvatarFile upload avatar file
|
||||
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
|
||||
func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -98,7 +105,7 @@ func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
|
||||
return us.uploadFile(ctx, file, avatarFilePath)
|
||||
}
|
||||
|
||||
func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
|
||||
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileName string, size int) (
|
||||
avatarfile []byte, err error) {
|
||||
if size > 1024 {
|
||||
size = 1024
|
||||
@@ -151,7 +158,7 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
|
||||
func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
|
||||
url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
@@ -177,7 +184,7 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
|
||||
return us.uploadFile(ctx, file, avatarFilePath)
|
||||
}
|
||||
|
||||
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
|
||||
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
|
||||
url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
@@ -204,7 +211,7 @@ func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
|
||||
return us.uploadFile(ctx, file, avatarFilePath)
|
||||
}
|
||||
|
||||
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
|
||||
func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
|
||||
url string, err error) {
|
||||
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
@@ -230,7 +237,7 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (us *UploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
|
||||
func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
|
||||
url string, err error) {
|
||||
_ = plugin.CallStorage(func(fn plugin.Storage) error {
|
||||
resp := fn.UploadFile(ctx, source)
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
.page-main {
|
||||
max-width: 70%;
|
||||
}
|
||||
.page-right-side {
|
||||
flex: none;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--bs-gray-300);
|
||||
min-height: calc(100vh - 62px - 74px);
|
||||
}
|
||||
|
||||
// lg
|
||||
@media screen and (max-width: 1199.9px) {
|
||||
.page-main {
|
||||
max-width: 100%;
|
||||
}
|
||||
.page-right-side {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// md
|
||||
@media screen and (max-width: 991.9px) {
|
||||
.line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,15 @@ const Index: FC<IProps> = ({ avatar, size, className, searchStr = '' }) => {
|
||||
url = `${avatar.custom}?${searchStr}`;
|
||||
}
|
||||
|
||||
const roundedCls =
|
||||
className && className.indexOf('rounded') !== -1 ? '' : 'rounded';
|
||||
|
||||
return (
|
||||
<img
|
||||
src={url || DefaultAvatar}
|
||||
width={size}
|
||||
height={size}
|
||||
className={classNames('rounded', className)}
|
||||
className={classNames(roundedCls, className)}
|
||||
alt=""
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -94,13 +94,17 @@ export function htmlRender(el: HTMLElement | null) {
|
||||
|
||||
el.querySelectorAll('.language-mermaid').forEach((pre) => {
|
||||
const flag = Date.now();
|
||||
mermaid.render(`theGraph${flag}`, pre.textContent, function (svgCode) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-center';
|
||||
p.innerHTML = svgCode;
|
||||
mermaid.render(
|
||||
`theGraph${flag}`,
|
||||
pre.textContent || '',
|
||||
function (svgCode) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-center';
|
||||
p.innerHTML = svgCode;
|
||||
|
||||
pre.parentNode?.replaceChild(p, pre);
|
||||
});
|
||||
pre.parentNode?.replaceChild(p, pre);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
import('katex/contrib/auto-render/auto-render').then(
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
|
||||
#sideNav {
|
||||
.nav-wrap {
|
||||
position: sticky;
|
||||
width: auto;
|
||||
top: 62px;
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - 74px - 62px - 24px);
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.nav {
|
||||
max-width: 172px;
|
||||
max-width: calc(100% - 24px);
|
||||
}
|
||||
.nav-link {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
@@ -14,6 +23,15 @@
|
||||
color: black;
|
||||
background-color: var(--bs-gray-200);
|
||||
}
|
||||
.side-nav-right-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--bs-gray-300);
|
||||
min-height: calc(100vh - 62px - 74px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991.9px) {
|
||||
@@ -21,5 +39,8 @@
|
||||
.nav {
|
||||
max-width: 100%;
|
||||
}
|
||||
.side-nav-right-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,53 +20,55 @@ const Index: FC = () => {
|
||||
lg={3}
|
||||
md={12}
|
||||
className={classnames(
|
||||
'pt-4 position-relative',
|
||||
visible ? 'fade-in' : 'd-none d-lg-block',
|
||||
'position-relative',
|
||||
visible ? '' : 'd-none d-lg-block',
|
||||
)}
|
||||
id="sideNav">
|
||||
<Nav variant="pills" className="flex-column">
|
||||
<NavLink
|
||||
to="/questions"
|
||||
className={({ isActive }) =>
|
||||
isActive || pathname === '/' ? 'nav-link active' : 'nav-link'
|
||||
}>
|
||||
<Icon name="question-circle-fill" className="me-2" />
|
||||
<span>{t('header.nav.question')}</span>
|
||||
</NavLink>
|
||||
<div className="nav-wrap pt-4">
|
||||
<Nav variant="pills" className="flex-column">
|
||||
<NavLink
|
||||
to="/questions"
|
||||
className={({ isActive }) =>
|
||||
isActive || pathname === '/' ? 'nav-link active' : 'nav-link'
|
||||
}>
|
||||
<Icon name="question-circle-fill" className="me-2" />
|
||||
<span>{t('header.nav.question')}</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/tags" className="nav-link">
|
||||
<Icon name="tags-fill" className="me-2" />
|
||||
<span>{t('header.nav.tag')}</span>
|
||||
</NavLink>
|
||||
<NavLink to="/tags" className="nav-link">
|
||||
<Icon name="tags-fill" className="me-2" />
|
||||
<span>{t('header.nav.tag')}</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to="/users" className="nav-link">
|
||||
<Icon name="people-fill" className="me-2" />
|
||||
<span>{t('header.nav.user')}</span>
|
||||
</NavLink>
|
||||
<NavLink to="/users" className="nav-link">
|
||||
<Icon name="people-fill" className="me-2" />
|
||||
<span>{t('header.nav.user')}</span>
|
||||
</NavLink>
|
||||
|
||||
{can_revision || userInfo?.role_id === 2 ? (
|
||||
<>
|
||||
<div className="py-2 px-3 mt-3 small fw-bold">
|
||||
{t('header.nav.moderation')}
|
||||
</div>
|
||||
{can_revision && (
|
||||
<NavLink to="/review" className="nav-link">
|
||||
<span>{t('header.nav.review')}</span>
|
||||
<span className="float-end">
|
||||
{revision > 99 ? '99+' : revision > 0 ? revision : ''}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
{can_revision || userInfo?.role_id === 2 ? (
|
||||
<>
|
||||
<div className="py-2 px-3 mt-3 small fw-bold">
|
||||
{t('header.nav.moderation')}
|
||||
</div>
|
||||
{can_revision && (
|
||||
<NavLink to="/review" className="nav-link">
|
||||
<span>{t('header.nav.review')}</span>
|
||||
<span className="float-end">
|
||||
{revision > 99 ? '99+' : revision > 0 ? revision : ''}
|
||||
</span>
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
{userInfo?.role_id === 2 ? (
|
||||
<NavLink to="/admin" className="nav-link">
|
||||
{t('header.nav.admin')}
|
||||
</NavLink>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Nav>
|
||||
<div className="line" />
|
||||
{userInfo?.role_id === 2 ? (
|
||||
<NavLink to="/admin" className="nav-link">
|
||||
{t('header.nav.admin')}
|
||||
</NavLink>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Nav>
|
||||
</div>
|
||||
<div className="side-nav-right-line" />
|
||||
</Col>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -322,7 +322,7 @@ const Ask = () => {
|
||||
<div className="pt-4 mb-5">
|
||||
<h3 className="mb-4">{isEdit ? t('edit_title') : t('title')}</h3>
|
||||
<Row>
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{isEdit && (
|
||||
<Form.Group controlId="revision" className="mb-3">
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.people-dropdown {
|
||||
.dropdown-menu {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: #212529;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { Dropdown, Form } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -6,21 +6,31 @@ import { loggedUserInfoStore } from '@/stores';
|
||||
import { userSearchByName } from '@/services';
|
||||
import { Avatar } from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
import './PeopleDropdown.scss';
|
||||
|
||||
interface Props {
|
||||
selectedPeople: Type.UserInfoBase[] | undefined;
|
||||
onSelect: (people: Type.UserInfoBase) => void;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
|
||||
const Index: FC<Props> = ({
|
||||
selectedPeople = [],
|
||||
visible = false,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { user: currentUser } = loggedUserInfoStore();
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'invite_to_answer',
|
||||
});
|
||||
const [toggleState, setToggleState] = useState(false);
|
||||
const [peopleList, setPeopleList] = useState<Type.UserInfoBase[]>([]);
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const filterAndSetPeople = (source) => {
|
||||
if (!toggleState) {
|
||||
return;
|
||||
}
|
||||
const filteredPeople: Type.UserInfoBase[] = [];
|
||||
source.forEach((p) => {
|
||||
if (currentUser && currentUser.username === p.username) {
|
||||
@@ -34,56 +44,109 @@ const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
|
||||
setPeopleList(filteredPeople);
|
||||
};
|
||||
|
||||
const searchPeople = (evt) => {
|
||||
const name = evt.target.value;
|
||||
if (!name) {
|
||||
const searchPeople = (s) => {
|
||||
if (!s) {
|
||||
setPeopleList([]);
|
||||
return;
|
||||
}
|
||||
userSearchByName(name).then((resp) => {
|
||||
userSearchByName(s).then((resp) => {
|
||||
filterAndSetPeople(resp);
|
||||
});
|
||||
};
|
||||
const handleSearch = (evt) => {
|
||||
const s = evt.target.value;
|
||||
setSearchValue(s);
|
||||
searchPeople(s);
|
||||
};
|
||||
|
||||
const updateCurrentIndex = (pl: number) => {
|
||||
let curIndex = currentIndex;
|
||||
if (currentIndex >= pl) {
|
||||
curIndex = Math.max(pl - 1, 0);
|
||||
}
|
||||
setCurrentIndex(curIndex);
|
||||
};
|
||||
|
||||
const handleSelect = (idx) => {
|
||||
if (idx < 0 || idx >= peopleList.length) {
|
||||
return;
|
||||
}
|
||||
const people = peopleList[idx];
|
||||
if (people) {
|
||||
updateCurrentIndex(peopleList.length - 1);
|
||||
onSelect(people);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (evt) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!peopleList?.length) {
|
||||
return;
|
||||
}
|
||||
const { keyCode } = evt;
|
||||
if (keyCode === 38 && currentIndex > 0) {
|
||||
setCurrentIndex(currentIndex - 1);
|
||||
}
|
||||
if (keyCode === 40 && currentIndex < peopleList.length - 1) {
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
}
|
||||
|
||||
if (keyCode === 13 && currentIndex > -1) {
|
||||
evt.preventDefault();
|
||||
handleSelect(currentIndex);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
filterAndSetPeople(peopleList);
|
||||
}, [selectedPeople]);
|
||||
|
||||
return (
|
||||
useEffect(() => {
|
||||
searchPeople(searchValue);
|
||||
}, [toggleState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && toggleState) {
|
||||
setToggleState(false);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
return visible ? (
|
||||
<Dropdown
|
||||
className="d-inline-flex"
|
||||
show={toggleState}
|
||||
className="d-inline-flex people-dropdown"
|
||||
align="end"
|
||||
onSelect={handleSelect}
|
||||
onKeyDown={handleKeyDown}
|
||||
onToggle={setToggleState}>
|
||||
<Dropdown.Toggle
|
||||
className="m-1 no-toggle"
|
||||
size="sm"
|
||||
variant="outline-secondary">
|
||||
{t('add')} +
|
||||
<span className="me-1">+</span>
|
||||
{t('add')}
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Menu show={toggleState}>
|
||||
<Dropdown.Header className="px-2 py-0">
|
||||
<Form.Control
|
||||
autoFocus
|
||||
placeholder={t('search')}
|
||||
onChange={searchPeople}
|
||||
/>
|
||||
{toggleState ? (
|
||||
<Form.Control
|
||||
autoFocus
|
||||
placeholder={t('search')}
|
||||
value={searchValue}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
) : null}
|
||||
</Dropdown.Header>
|
||||
{peopleList.map((p, idx) => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={p.username}
|
||||
eventKey={idx}
|
||||
active={idx === currentIndex}
|
||||
className={idx === 0 ? 'mt-2' : ''}>
|
||||
<div className="d-flex align-items-center text-nowrap">
|
||||
<Avatar avatar={p.avatar} size="24" />
|
||||
<Avatar avatar={p.avatar} size="24" className="rounded-1" />
|
||||
<span className="mx-2">{p.display_name}</span>
|
||||
<small className="text-secondary">@{p.username}</small>
|
||||
</div>
|
||||
@@ -92,7 +155,7 @@ const Index: FC<Props> = ({ selectedPeople = [], onSelect }) => {
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
||||
export default Index;
|
||||
|
||||
@@ -73,9 +73,15 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
}, [questionId]);
|
||||
|
||||
const showAddButton = editing && (!users || users.length < MAX_ASK_NUMBER);
|
||||
const showInviteDesc = !editing && users?.length === 0;
|
||||
const showInviteFeat = !editing && users?.length === 0;
|
||||
const showInviteButton = showInviteFeat && !readOnly;
|
||||
const showEditButton = !readOnly && !editing && users?.length;
|
||||
const showSaveButton = !readOnly && editing;
|
||||
const showEmpty = readOnly && users?.length === 0;
|
||||
|
||||
if (showEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mt-4">
|
||||
@@ -113,9 +119,9 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
<span className="text-nowrap ms-2">{user.display_name}</span>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
|
||||
<span
|
||||
className="ps-1 pe-1 me-n1"
|
||||
className="px-1 me-n1"
|
||||
onClick={() => removeInviteUser(user)}>
|
||||
x
|
||||
×
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -125,22 +131,21 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
key={user.username}
|
||||
to={`/users/${user.username}`}
|
||||
className="mx-2 my-1 d-inline-flex flex-nowrap">
|
||||
<Avatar avatar={user.avatar} size="24" />
|
||||
<span className="text-nowrap ms-2">{user.display_name}</span>
|
||||
<Avatar avatar={user.avatar} size="24" className="rounded-1" />
|
||||
<small className="text-nowrap ms-2">{user.display_name}</small>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{showAddButton ? (
|
||||
<PeopleDropdown
|
||||
selectedPeople={users}
|
||||
onSelect={updateInviteUsers}
|
||||
/>
|
||||
) : null}
|
||||
<PeopleDropdown
|
||||
visible={showAddButton}
|
||||
selectedPeople={users}
|
||||
onSelect={updateInviteUsers}
|
||||
/>
|
||||
</div>
|
||||
{showInviteDesc ? (
|
||||
{showInviteFeat ? (
|
||||
<>
|
||||
<div className="text-muted">{t('desc')}</div>
|
||||
{readOnly ? null : (
|
||||
{showInviteButton ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline-primary"
|
||||
@@ -148,7 +153,7 @@ const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
onClick={() => setEditing(true)}>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</Card.Body>
|
||||
|
||||
@@ -212,7 +212,7 @@ const Index = () => {
|
||||
|
||||
return (
|
||||
<Row className="questionDetailPage pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
{question?.operation?.level && <Alert data={question.operation} />}
|
||||
{isLoading ? (
|
||||
<ContentLoader />
|
||||
|
||||
@@ -187,7 +187,7 @@ const Index = () => {
|
||||
<div className="pt-4 mb-5 edit-answer-wrap">
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<Row>
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<a
|
||||
href={pathFactory.questionLanding(qid, data?.question.url_title)}
|
||||
target="_blank"
|
||||
|
||||
@@ -52,7 +52,7 @@ const Questions: FC = () => {
|
||||
usePageTags({ title: pageTitle, subtitle: slogan });
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<QuestionList
|
||||
source="questions"
|
||||
data={listData}
|
||||
|
||||
@@ -125,7 +125,7 @@ const Index: FC = () => {
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<h3 className="mb-4">{t('review')}</h3>
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
{!noTasks && ro && (
|
||||
<>
|
||||
<Alert variant="secondary">
|
||||
|
||||
@@ -39,7 +39,7 @@ const Index = () => {
|
||||
});
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<Head data={extra} />
|
||||
<SearchHead sort={order} count={count} />
|
||||
<ListGroup className="rounded-0 mb-5">
|
||||
|
||||
@@ -142,7 +142,7 @@ const Index = () => {
|
||||
<div className="pt-4 mb-5">
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<Row>
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="display_name" className="mb-3">
|
||||
<Form.Label>{t('form.fields.display_name.label')}</Form.Label>
|
||||
|
||||
@@ -17,18 +17,24 @@ import {
|
||||
useQuerySynonymsTags,
|
||||
useQuestionList,
|
||||
} from '@/services';
|
||||
import QuestionList from '@/components/QuestionList';
|
||||
import QuestionList, { QUESTION_ORDER_KEYS } from '@/components/QuestionList';
|
||||
import HotQuestions from '@/components/HotQuestions';
|
||||
import { escapeRemove, guard } from '@/utils';
|
||||
import { escapeRemove, guard, Storage } from '@/utils';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { QUESTIONS_ORDER_STORAGE_KEY } from '@/common/constants';
|
||||
|
||||
const Questions: FC = () => {
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'tags' });
|
||||
const navigate = useNavigate();
|
||||
const routeParams = useParams();
|
||||
const curTagName = routeParams.tagName || '';
|
||||
const [urlSearchParams] = useSearchParams();
|
||||
const curOrder = urlSearchParams.get('order') || 'active';
|
||||
const storageOrder = Storage.get(QUESTIONS_ORDER_STORAGE_KEY);
|
||||
const curOrder =
|
||||
urlSearchParams.get('order') || storageOrder || QUESTION_ORDER_KEYS[0];
|
||||
if (curOrder !== storageOrder) {
|
||||
Storage.set(QUESTIONS_ORDER_STORAGE_KEY, curOrder);
|
||||
}
|
||||
const curPage = Number(urlSearchParams.get('page')) || 1;
|
||||
const reqParams: Type.QueryQuestionsReq = {
|
||||
page_size: 20,
|
||||
@@ -101,7 +107,7 @@ const Questions: FC = () => {
|
||||
});
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
{isLoading || listLoading ? (
|
||||
<div className="tag-box mb-5 placeholder-glow">
|
||||
<div className="mb-3 h3 placeholder" style={{ width: '120px' }} />
|
||||
@@ -148,7 +154,12 @@ const Questions: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<QuestionList source="tag" data={listData} isLoading={listLoading} />
|
||||
<QuestionList
|
||||
source="tag"
|
||||
data={listData}
|
||||
order={curOrder}
|
||||
isLoading={listLoading}
|
||||
/>
|
||||
</Col>
|
||||
<Col className="page-right-side mt-4 mt-xl-0">
|
||||
<CustomSidebar />
|
||||
@@ -159,4 +170,4 @@ const Questions: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Questions;
|
||||
export default Index;
|
||||
|
||||
@@ -191,7 +191,7 @@ const Index = () => {
|
||||
<div className="pt-4 mb-5">
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<Row>
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="revision" className="mb-3">
|
||||
<Form.Label>{t('form.fields.revision.label')}</Form.Label>
|
||||
|
||||
@@ -133,7 +133,7 @@ const TagIntroduction = () => {
|
||||
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<h3 className="mb-3">
|
||||
<Link
|
||||
to={pathFactory.tagLanding(tagInfo.slug_name)}
|
||||
|
||||
@@ -65,12 +65,13 @@ const Index: FC = () => {
|
||||
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')}
|
||||
{ucAgent?.agent_info?.display_name} {t('login')}
|
||||
</Card.Title>
|
||||
{qrcodeDataUrl ? (
|
||||
<>
|
||||
@@ -82,7 +83,7 @@ const Index: FC = () => {
|
||||
/>
|
||||
<div className="text-secondary mt-3">
|
||||
{t('qrcode_login_tip', {
|
||||
agentName: ucAgent?.agent_info.display_name,
|
||||
agentName: ucAgent?.agent_info?.display_name,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,7 @@ const Index = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
const [searchParam] = useSearchParams();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
let agentName = ucAgent?.agent_info.name || '';
|
||||
let agentName = ucAgent?.agent_info?.name || '';
|
||||
if (searchParam.get('agent_name')) {
|
||||
agentName = searchParam.get('agent_name') || '';
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const Index: FC = () => {
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<h3 className="text-center pt-3 mb-3">
|
||||
{ucAgent?.agent_info.display_name} {t('login')}
|
||||
{ucAgent?.agent_info?.display_name} {t('login')}
|
||||
</h3>
|
||||
<p className="text-danger text-center">
|
||||
{t('login_failed_email_tip')}
|
||||
|
||||
@@ -13,7 +13,7 @@ const Index = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
const [searchParam] = useSearchParams();
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
let agentName = ucAgent?.agent_info.name || '';
|
||||
let agentName = ucAgent?.agent_info?.name || '';
|
||||
if (searchParam.get('agent_name')) {
|
||||
agentName = searchParam.get('agent_name') || '';
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const Notifications = () => {
|
||||
});
|
||||
return (
|
||||
<Row className="pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between mb-3">
|
||||
<ButtonGroup size="sm">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FC, memo } from 'react';
|
||||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { FormatTime } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
@@ -11,7 +10,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ visible, data }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'personal' });
|
||||
if (!visible || !data?.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -48,9 +46,7 @@ const Index: FC<Props> = ({ visible, data }) => {
|
||||
{item.title}
|
||||
</a>
|
||||
<div className="d-flex align-items-center small text-secondary">
|
||||
<span>
|
||||
{t(item.rank_type, { keyPrefix: 'personal.rank_type' })}
|
||||
</span>
|
||||
<span>{item.rank_type}</span>
|
||||
<span className="split-dot" />
|
||||
<FormatTime time={item.created_at} className="me-4" />
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ const Personal: FC = () => {
|
||||
{userInfo?.status !== 'normal' && userInfo?.status_msg && (
|
||||
<Alert data={userInfo?.status_msg} />
|
||||
)}
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<UserInfo data={userInfo as UserInfoRes} />
|
||||
</Col>
|
||||
<Col
|
||||
@@ -85,7 +85,7 @@ const Personal: FC = () => {
|
||||
|
||||
<Row>
|
||||
<NavBar tabName={tabName} slug={username} isSelf={isSelf} />
|
||||
<Col className="flex-auto">
|
||||
<Col className="page-main flex-auto">
|
||||
<Overview
|
||||
visible={tabName === 'overview'}
|
||||
introduction={userInfo?.bio_html || ''}
|
||||
|
||||
@@ -10,6 +10,47 @@ const equalToCurrentHref = (target: string, base?: string) => {
|
||||
const targetUrl = new URL(target, base);
|
||||
return targetUrl.toString() === window.location.href;
|
||||
};
|
||||
const matchToCurrentHref = (target: string) => {
|
||||
target = (target || '').trim();
|
||||
// Empty string or `/` can match any path
|
||||
if (!target || target === '/') {
|
||||
return true;
|
||||
}
|
||||
const { pathname, search, hash } = window.location;
|
||||
const tPart = target.split('?');
|
||||
|
||||
/**
|
||||
* With the current requirements, `hash` and `search` can simply be matched
|
||||
* Later extended to field-by-field matching if necessary
|
||||
*/
|
||||
if (tPart[1]) {
|
||||
const tChip = tPart[1].split('#');
|
||||
const tSearch = tChip[0] || '';
|
||||
const tHash = tChip[1] || '';
|
||||
if (tHash && hash.indexOf(tHash) === -1) {
|
||||
return false;
|
||||
}
|
||||
if (tSearch && search.indexOf(tSearch) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As determination above, `tPart[0]` must be a valid string
|
||||
*/
|
||||
let pathMatch = true;
|
||||
const tPath = tPart[0].split('/').filter((_) => !!_);
|
||||
const lPath = pathname.split('/').filter((_) => !!_);
|
||||
|
||||
tPath.forEach((p, i) => {
|
||||
const lp = lPath[i];
|
||||
if (p !== lp) {
|
||||
pathMatch = false;
|
||||
}
|
||||
});
|
||||
|
||||
return pathMatch;
|
||||
};
|
||||
|
||||
const storageLoginRedirect = () => {
|
||||
const { pathname } = window.location;
|
||||
@@ -136,5 +177,6 @@ export const floppyNavigation = {
|
||||
isRoutableLink,
|
||||
handleRouteLinkClick,
|
||||
equalToCurrentHref,
|
||||
matchToCurrentHref,
|
||||
storageLoginRedirect,
|
||||
};
|
||||
|
||||
@@ -94,13 +94,15 @@ export const IGNORE_PATH_LIST = [
|
||||
'/user-center/',
|
||||
];
|
||||
|
||||
export const isIgnoredPath = (ignoredPath: string | string[]) => {
|
||||
export const isIgnoredPath = (ignoredPath?: string | string[]) => {
|
||||
if (!ignoredPath) {
|
||||
ignoredPath = IGNORE_PATH_LIST;
|
||||
}
|
||||
if (!Array.isArray(ignoredPath)) {
|
||||
ignoredPath = [ignoredPath];
|
||||
}
|
||||
const { pathname } = window.location;
|
||||
const matchingPath = ignoredPath.find((_) => {
|
||||
return pathname.indexOf(_) !== -1;
|
||||
const matchingPath = ignoredPath.find((p) => {
|
||||
return floppyNavigation.matchToCurrentHref(p);
|
||||
});
|
||||
return !!matchingPath;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user