feat: add top hide function

This commit is contained in:
shuai 2023-04-13 18:43:48 +08:00
parent 5361f4e127
commit 17b7e9c7b8
14 changed files with 172 additions and 28 deletions

View File

@ -789,6 +789,7 @@ ui:
btn: Add question btn: Add question
answers: answers answers: answers
question_detail: question_detail:
action: Action
Asked: Asked Asked: Asked
asked: asked asked: asked
update: Modified update: Modified
@ -827,13 +828,14 @@ ui:
li1_2: Back up any statements you make with references or personal experience. li1_2: Back up any statements you make with references or personal experience.
header_2: But <strong>avoid</strong> ... header_2: But <strong>avoid</strong> ...
li2_1: Asking for help, seeking clarification, or responding to other answers. li2_1: Asking for help, seeking clarification, or responding to other answers.
reopen: reopen:
confirm_btn: Reopen confirm_btn: Reopen
title: Reopen this post title: Reopen this post
content: Are you sure you want to reopen? content: Are you sure you want to reopen?
success: This post has been reopened pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete: delete:
title: Delete this post title: Delete this post
question: >- question: >-
@ -847,7 +849,6 @@ ui:
of accepted answers can result in your account being blocked from answering. of accepted answers can result in your account being blocked from answering.
Are you sure you wish to delete? Are you sure you wish to delete?
other: Are you sure you wish to delete? other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted tip_answer_deleted: This answer has been deleted
btns: btns:
confirm: Confirm confirm: Confirm
@ -863,6 +864,7 @@ ui:
reject: Reject reject: Reject
skip: Skip skip: Skip
discard_draft: Discard draft discard_draft: Discard draft
pinned: Pinned
search: search:
title: Search Results title: Search Results
keywords: Keywords keywords: Keywords
@ -1434,6 +1436,10 @@ ui:
closed: closed closed: closed
reopened: reopened reopened: reopened
created: created created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for" title: "History for"
tag_title: "Timeline for" tag_title: "Timeline for"
show_votes: "Show votes" show_votes: "Show votes"
@ -1459,5 +1465,9 @@ ui:
draft: draft:
discard_confirm: Are you sure you want to discard your draft? discard_confirm: Are you sure you want to discard your draft?
messages: messages:
post_deleted: This post has been deleted. post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -594,6 +594,10 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
'upvote', 'upvote',
'reopened', 'reopened',
'closed', 'closed',
'pin',
'unpin',
'show',
'hide',
]; ];
export const SYSTEM_AVATAR_OPTIONS = [ export const SYSTEM_AVATAR_OPTIONS = [

View File

@ -526,3 +526,8 @@ export interface User {
display_name: string; display_name: string;
avatar: string; avatar: string;
} }
export interface QuestionOperationReq {
id: string;
operation: 'pin' | 'unpin' | 'hide' | 'show';
}

View File

@ -51,7 +51,7 @@ const Index = ({
'd-flex align-items-start flex-column flex-md-row', 'd-flex align-items-start flex-column flex-md-row',
className, className,
)}> )}>
<div> <div className="w-100">
<div <div
className={classNames('custom-form-control', { className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg, 'is-invalid': validationErrorMsg,

View File

@ -37,7 +37,7 @@ const Index = ({ userName, onSendReply, onCancel, mode }) => {
{t('reply_to')} {userName} {t('reply_to')} {userName}
</div> </div>
<div className="d-flex mb-1 align-items-start flex-column flex-md-row"> <div className="d-flex mb-1 align-items-start flex-column flex-md-row">
<div> <div className="w-100">
<div <div
className={classNames('custom-form-control', { className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg, 'is-invalid': validationErrorMsg,

View File

@ -34,7 +34,9 @@ const ActivateScriptNodes = (el, part) => {
} }
scriptList?.forEach((so) => { scriptList?.forEach((so) => {
const script = document.createElement('script'); const script = document.createElement('script');
script.text = so.text; script.text = `(() => {
${so.text}
})();`;
for (let i = 0; i < so.attributes.length; i += 1) { for (let i = 0; i < so.attributes.length; i += 1) {
const attr = so.attributes[i]; const attr = so.attributes[i];
script.setAttribute(attr.name, attr.value); script.setAttribute(attr.name, attr.value);

View File

@ -8,15 +8,24 @@ interface IProps {
name: string; name: string;
className?: string; className?: string;
size?: string; size?: string;
title?: string;
onClick?: () => void; onClick?: () => void;
} }
const Icon: FC<IProps> = ({ type = 'br', name, className, size, onClick }) => { const Icon: FC<IProps> = ({
type = 'br',
name,
className,
size,
onClick,
title = '',
}) => {
return ( return (
<i <i
className={classNames(type, `bi-${name}`, className)} className={classNames(type, `bi-${name}`, className)}
style={{ ...(size && { fontSize: size }) }} style={{ ...(size && { fontSize: size }) }}
onClick={onClick} onClick={onClick}
onKeyDown={onClick} onKeyDown={onClick}
title={title}
/> />
); );
}; };

View File

@ -1,19 +1,22 @@
import { memo, FC } from 'react'; import { memo, FC } from 'react';
import { Button } from 'react-bootstrap'; import { Button, Dropdown } from 'react-bootstrap';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal } from '@/components'; import { Modal } from '@/components';
import { useReportModal, useToast } from '@/hooks'; import { useReportModal, useToast } from '@/hooks';
import { QuestionOperationReq } from '@/common/interface';
import Share from '../Share'; import Share from '../Share';
import { import {
deleteQuestion, deleteQuestion,
deleteAnswer, deleteAnswer,
editCheck, editCheck,
reopenQuestion, reopenQuestion,
questionOpetation,
} from '@/services'; } from '@/services';
import { tryNormalLogged } from '@/utils/guard'; import { tryNormalLogged } from '@/utils/guard';
import { floppyNavigation } from '@/utils'; import { floppyNavigation } from '@/utils';
import { toastStore } from '@/stores';
interface IProps { interface IProps {
type: 'answer' | 'question'; type: 'answer' | 'question';
@ -78,7 +81,7 @@ const Index: FC<IProps> = ({
id: qid, id: qid,
}).then(() => { }).then(() => {
toast.onShow({ toast.onShow({
msg: t('tip_question_deleted'), msg: t('post_deleted', { keyPrefix: 'messages' }),
variant: 'success', variant: 'success',
}); });
callback?.('delete_question'); callback?.('delete_question');
@ -134,7 +137,7 @@ const Index: FC<IProps> = ({
question_id: qid, question_id: qid,
}).then(() => { }).then(() => {
toast.onShow({ toast.onShow({
msg: t('success', { keyPrefix: 'question_detail.reopen' }), msg: t('post_reopen', { keyPrefix: 'messages' }),
variant: 'success', variant: 'success',
}); });
refreshQuestion(); refreshQuestion();
@ -143,6 +146,51 @@ const Index: FC<IProps> = ({
}); });
}; };
const handleCommon = async (params) => {
await questionOpetation(params);
let msg = '';
if (params.operation === 'pin') {
msg = t('post_pin', { keyPrefix: 'messages' });
}
if (params.operation === 'unpin') {
msg = t('post_unpin', { keyPrefix: 'messages' });
}
if (params.operation === 'hide') {
msg = t('post_hide_list', { keyPrefix: 'messages' });
}
if (params.operation === 'show') {
msg = t('post_show_list', { keyPrefix: 'messages' });
}
toastStore.getState().show({
msg,
variant: 'success',
});
setTimeout(() => {
refreshQuestion();
}, 100);
};
const handlOtherActions = (action) => {
const params: QuestionOperationReq = {
id: qid,
operation: action,
};
if (action === 'pin') {
Modal.confirm({
title: t('title', { keyPrefix: 'question_detail.pin' }),
content: t('content', { keyPrefix: 'question_detail.pin' }),
cancelBtnVariant: 'link',
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.pin' }),
onConfirm: () => {
handleCommon(params);
},
});
} else {
handleCommon(params);
}
};
const handleAction = (action) => { const handleAction = (action) => {
if (!tryNormalLogged(true)) { if (!tryNormalLogged(true)) {
return; return;
@ -162,8 +210,33 @@ const Index: FC<IProps> = ({
if (action === 'reopen') { if (action === 'reopen') {
handleReopen(); handleReopen();
} }
if (
action === 'pin' ||
action === 'unpin' ||
action === 'hide' ||
action === 'show'
) {
handlOtherActions(action);
}
}; };
const firstAction =
memberActions?.filter(
(v) =>
v.action === 'report' || v.action === 'edit' || v.action === 'delete',
) || [];
const secondAction =
memberActions?.filter(
(v) =>
v.action === 'close' ||
v.action === 'reopen' ||
v.action === 'pin' ||
v.action === 'unpin' ||
v.action === 'hide' ||
v.action === 'show',
) || [];
return ( return (
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<Share <Share
@ -173,13 +246,13 @@ const Index: FC<IProps> = ({
title={title} title={title}
slugTitle={slugTitle} slugTitle={slugTitle}
/> />
{memberActions?.map((item) => { {firstAction?.map((item) => {
if (item.action === 'edit') { if (item.action === 'edit') {
return ( return (
<Link <Link
key={item.action} key={item.action}
to={editUrl} to={editUrl}
className="link-secondary p-0 fs-14 me-3" className="link-secondary p-0 fs-14 ms-3"
onClick={(evt) => handleEdit(evt, editUrl)} onClick={(evt) => handleEdit(evt, editUrl)}
style={{ lineHeight: '23px' }}> style={{ lineHeight: '23px' }}>
{item.name} {item.name}
@ -190,12 +263,32 @@ const Index: FC<IProps> = ({
<Button <Button
key={item.action} key={item.action}
variant="link" variant="link"
className="link-secondary p-0 fs-14 me-3" className="link-secondary p-0 fs-14 ms-3"
onClick={() => handleAction(item.action)}> onClick={() => handleAction(item.action)}>
{item.name} {item.name}
</Button> </Button>
); );
})} })}
{secondAction.length > 0 && (
<Dropdown className="ms-3">
<Dropdown.Toggle
variant="link"
className="link-secondary p-0 fs-14 no-toggle">
{t('action', { keyPrefix: 'question_detail' })}
</Dropdown.Toggle>
<Dropdown.Menu>
{secondAction.map((item) => {
return (
<Dropdown.Item
key={item.action}
onClick={() => handleAction(item.action)}>
{item.name}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
)}
</div> </div>
); );
}; };

View File

@ -14,6 +14,7 @@ import {
QueryGroup, QueryGroup,
QuestionListLoader, QuestionListLoader,
Counts, Counts,
Icon,
} from '@/components'; } from '@/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [ const QuestionOrderKeys: Type.QuestionOrderBy[] = [
@ -62,6 +63,13 @@ const QuestionList: FC<Props> = ({ source, data, isLoading = false }) => {
key={li.id} key={li.id}
className="bg-transparent py-3 px-0 border-start-0 border-end-0"> className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break"> <h5 className="text-wrap text-break">
{li.pin === 2 && (
<Icon
name="pin-fill"
className="me-1"
title={t('pinned', { keyPrefix: 'btns' })}
/>
)}
<NavLink <NavLink
to={pathFactory.questionLanding(li.id, li.url_title)} to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark"> className="link-dark">

View File

@ -71,7 +71,7 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
<Dropdown.Toggle <Dropdown.Toggle
id="dropdown-share" id="dropdown-share"
as="a" as="a"
className="no-toggle fs-14 link-secondary pointer me-3" className="no-toggle fs-14 link-secondary pointer"
onClick={() => setShow(true)} onClick={() => setShow(true)}
style={{ lineHeight: '23px' }}> style={{ lineHeight: '23px' }}>
{t('share.name')} {t('share.name')}

View File

@ -38,7 +38,7 @@ const TagSelector: FC<IProps> = ({
const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]); const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]);
const [currentIndex, setCurrentIndex] = useState<number>(0); const [currentIndex, setCurrentIndex] = useState<number>(0);
const [repeatIndex, setRepeatIndex] = useState(-1); const [repeatIndex, setRepeatIndex] = useState(-1);
const [tag, setTag] = useState<string>(''); const [searchValue, setSearchValue] = useState<string>('');
const [tags, setTags] = useState<Type.Tag[] | null>(null); const [tags, setTags] = useState<Type.Tag[] | null>(null);
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' }); const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
const [visibleMenu, setVisibleMenu] = useState(false); const [visibleMenu, setVisibleMenu] = useState(false);
@ -101,12 +101,12 @@ const TagSelector: FC<IProps> = ({
const fetchTags = (str) => { const fetchTags = (str) => {
queryTags(str).then((res) => { queryTags(str).then((res) => {
const tagArray: Type.Tag[] = filterTags(res || []); const tagArray: Type.Tag[] = filterTags(res || []);
setTags(tagArray); setTags(tagArray?.length > 5 ? tagArray.slice(0, 5) : tagArray);
}); });
}; };
useEffect(() => { useEffect(() => {
fetchTags(tag); fetchTags(searchValue);
}, [visibleMenu]); }, [visibleMenu]);
const handleClick = (val: Type.Tag) => { const handleClick = (val: Type.Tag) => {
@ -146,7 +146,7 @@ const TagSelector: FC<IProps> = ({
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchStr = e.currentTarget.value.replace(';', ''); const searchStr = e.currentTarget.value.replace(';', '');
setTag(searchStr); setSearchValue(searchStr);
fetchTags(searchStr); fetchTags(searchStr);
}; };
@ -172,7 +172,7 @@ const TagSelector: FC<IProps> = ({
e.preventDefault(); e.preventDefault();
if (tags.length === 0) { if (tags.length === 0) {
tagModal.onShow(tag); tagModal.onShow(searchValue);
return; return;
} }
if (currentIndex <= tags.length - 1) { if (currentIndex <= tags.length - 1) {
@ -228,13 +228,14 @@ const TagSelector: FC<IProps> = ({
<FormControl <FormControl
placeholder={t('search_tag')} placeholder={t('search_tag')}
autoFocus autoFocus
value={tag} value={searchValue}
onChange={handleSearch} onChange={handleSearch}
/> />
</Form> </Form>
</Dropdown.Header> </Dropdown.Header>
)} )}
{showRequiredTagText && {!searchValue &&
showRequiredTagText &&
tags && tags &&
tags.filter((v) => v.recommend)?.length > 0 && ( tags.filter((v) => v.recommend)?.length > 0 && (
<h6 className="dropdown-header">{t('tag_required_text')}</h6> <h6 className="dropdown-header">{t('tag_required_text')}</h6>
@ -251,17 +252,17 @@ const TagSelector: FC<IProps> = ({
</Dropdown.Item> </Dropdown.Item>
); );
})} })}
{tag && tags && tags.length === 0 && ( {searchValue && tags && tags.length === 0 && (
<Dropdown.Item disabled className="text-secondary"> <Dropdown.Item disabled className="text-secondary">
{t('no_result')} {t('no_result')}
</Dropdown.Item> </Dropdown.Item>
)} )}
{!hiddenCreateBtn && tag && ( {!hiddenCreateBtn && searchValue && (
<Button <Button
variant="link" variant="link"
className="px-3 btn-no-border w-100 text-start" className="px-3 btn-no-border w-100 text-start"
onClick={() => { onClick={() => {
tagModal.onShow(tag); tagModal.onShow(searchValue);
}}> }}>
+ {t('create_btn')} + {t('create_btn')}
</Button> </Button>

View File

@ -11,6 +11,7 @@ import {
Comment, Comment,
FormatTime, FormatTime,
htmlRender, htmlRender,
Icon,
} from '@/components'; } from '@/components';
import { formatCount, guard } from '@/utils'; import { formatCount, guard } from '@/utils';
import { following } from '@/services'; import { following } from '@/services';
@ -65,6 +66,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
return ( return (
<div> <div>
<h1 className="h3 mb-3 text-wrap text-break"> <h1 className="h3 mb-3 text-wrap text-break">
{data?.pin === 2 && (
<Icon
name="pin-fill"
className="me-1"
title={t('pinned', { keyPrefix: 'btns' })}
/>
)}
<Link <Link
className="link-dark" className="link-dark"
reloadDocument reloadDocument

View File

@ -300,7 +300,7 @@ const Index: React.FC = () => {
</Form.Control.Feedback> </Form.Control.Feedback>
</Form.Group> </Form.Group>
<Form.Group className="mb-3"> <Form.Group controlId="avatar" className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label> <Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-3"> <div className="mb-3">
<Form.Select <Form.Select

View File

@ -278,3 +278,7 @@ export const markdownToHtml = (content: string) => {
export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => { export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => {
return request.post('/answer/api/v1/question/answer', params); return request.post('/answer/api/v1/question/answer', params);
}; };
export const questionOpetation = (params: Type.QuestionOperationReq) => {
return request.put('/answer/api/v1/question/operation', params);
};