mirror of https://gitee.com/answerdev/answer.git
commit
97f58483f5
|
@ -482,6 +482,11 @@ ui:
|
|||
all_read: Mark all as read
|
||||
show_more: Show more
|
||||
someone: Someone
|
||||
inbox_type:
|
||||
all: All
|
||||
posts: Posts
|
||||
invites: Invites
|
||||
votes: Votes
|
||||
suspended:
|
||||
title: Your Account has been Suspended
|
||||
until_time: "Your account was suspended until {{ time }}."
|
||||
|
@ -971,6 +976,12 @@ ui:
|
|||
title: Related Questions
|
||||
btn: Add question
|
||||
answers: answers
|
||||
invite_to_answer:
|
||||
title: People asked
|
||||
desc: Invite people who you think might know the answer.
|
||||
invite: Invite to answer
|
||||
add: Add people
|
||||
search: Search people
|
||||
question_detail:
|
||||
action: Action
|
||||
Asked: Asked
|
||||
|
@ -1037,6 +1048,7 @@ ui:
|
|||
btns:
|
||||
confirm: Confirm
|
||||
cancel: Cancel
|
||||
edit: Edit
|
||||
save: Save
|
||||
delete: Delete
|
||||
login: Log in
|
||||
|
|
|
@ -122,7 +122,7 @@ export interface UserInfoBase {
|
|||
*/
|
||||
status?: string;
|
||||
/** roles */
|
||||
role_id: RoleId;
|
||||
role_id?: RoleId;
|
||||
}
|
||||
|
||||
export interface UserInfoRes extends UserInfoBase {
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import { FC, memo, useEffect, useState } from 'react';
|
||||
import { Dropdown, Form } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { userSearchByName } from '@/services';
|
||||
import { Avatar } from '@/components';
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
selectedPeople: Type.UserInfoBase[] | undefined;
|
||||
onSelect: (people: Type.UserInfoBase) => void;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ selectedPeople = [], 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 filterAndSetPeople = (source) => {
|
||||
const filteredPeople: Type.UserInfoBase[] = [];
|
||||
source.forEach((p) => {
|
||||
if (currentUser && currentUser.username === p.username) {
|
||||
return;
|
||||
}
|
||||
if (selectedPeople.find((_) => _.username === p.username)) {
|
||||
return;
|
||||
}
|
||||
filteredPeople.push(p);
|
||||
});
|
||||
setPeopleList(filteredPeople);
|
||||
};
|
||||
|
||||
const searchPeople = (evt) => {
|
||||
const name = evt.target.value;
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
userSearchByName(name).then((resp) => {
|
||||
filterAndSetPeople(resp);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelect = (idx) => {
|
||||
const people = peopleList[idx];
|
||||
if (people) {
|
||||
onSelect(people);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
filterAndSetPeople(peopleList);
|
||||
}, [selectedPeople]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="d-inline-flex"
|
||||
show={toggleState}
|
||||
onSelect={handleSelect}
|
||||
onToggle={setToggleState}>
|
||||
<Dropdown.Toggle
|
||||
className="m-1 no-toggle"
|
||||
size="sm"
|
||||
variant="outline-secondary">
|
||||
{t('add')} +
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header className="px-2 pt-0">
|
||||
<Form.Control
|
||||
autoFocus
|
||||
placeholder={t('search')}
|
||||
onChange={searchPeople}
|
||||
/>
|
||||
</Dropdown.Header>
|
||||
{peopleList.map((p, idx) => {
|
||||
return (
|
||||
<Dropdown.Item key={p.username} eventKey={idx}>
|
||||
<div className="d-flex align-items-center text-nowrap">
|
||||
<Avatar avatar={p.avatar} size="24" />
|
||||
<span className="mx-2">{p.display_name}</span>
|
||||
<small className="text-secondary">@{p.username}</small>
|
||||
</div>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -0,0 +1,154 @@
|
|||
import { memo, FC, useState, useEffect } from 'react';
|
||||
import { Card, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '@/components';
|
||||
import { getInviteUser, putInviteUser } from '@/services';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
import PeopleDropdown from './PeopleDropdown';
|
||||
|
||||
interface Props {
|
||||
questionId: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
const Index: FC<Props> = ({ questionId, readOnly = false }) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'invite_to_answer',
|
||||
});
|
||||
const MAX_ASK_NUMBER = 5;
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [users, setUsers] = useState<Type.UserInfoBase[]>();
|
||||
|
||||
const initInviteUsers = () => {
|
||||
if (!questionId) {
|
||||
return;
|
||||
}
|
||||
getInviteUser(questionId)
|
||||
.then((resp) => {
|
||||
setUsers(resp);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!users) {
|
||||
setUsers([]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateInviteUsers = (user: Type.UserInfoBase) => {
|
||||
let userList = [user];
|
||||
if (users?.length) {
|
||||
userList = [...users, user];
|
||||
}
|
||||
setUsers(userList);
|
||||
};
|
||||
|
||||
const removeInviteUser = (user: Type.UserInfoBase) => {
|
||||
const inviteUsers = users!.filter((_) => {
|
||||
return _.username !== user.username;
|
||||
});
|
||||
setUsers(inviteUsers);
|
||||
};
|
||||
|
||||
const saveInviteUsers = () => {
|
||||
if (!users) {
|
||||
return;
|
||||
}
|
||||
const names = users.map((_) => {
|
||||
return _.username;
|
||||
});
|
||||
putInviteUser(questionId, names)
|
||||
.then(() => {
|
||||
setEditing(false);
|
||||
})
|
||||
.catch((ex) => {
|
||||
console.log('ex: ', ex);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
initInviteUsers();
|
||||
}, [questionId]);
|
||||
|
||||
const showAddButton = editing && (!users || users.length < MAX_ASK_NUMBER);
|
||||
const showInviteDesc = !editing && users?.length === 0;
|
||||
|
||||
return (
|
||||
<Card className="mt-4">
|
||||
<Card.Header className="text-nowrap d-flex justify-content-between text-capitalize">
|
||||
{t('title')}
|
||||
{!readOnly && editing ? (
|
||||
<Button onClick={saveInviteUsers} variant="link" className="p-0">
|
||||
{t('save', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
) : null}
|
||||
{!readOnly && !editing ? (
|
||||
<Button
|
||||
onClick={() => setEditing(true)}
|
||||
variant="link"
|
||||
className="p-0">
|
||||
{t('edit', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
) : null}
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex align-items-center flex-wrap',
|
||||
editing ? 'm-n1' : ' mx-n2 my-n1',
|
||||
)}>
|
||||
{users?.map((user) => {
|
||||
if (editing) {
|
||||
return (
|
||||
<Button
|
||||
key={user.username}
|
||||
className="m-1 d-inline-flex flex-nowrap"
|
||||
size="sm"
|
||||
variant="outline-secondary">
|
||||
<Avatar avatar={user.avatar} size="20" />
|
||||
<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"
|
||||
onClick={() => removeInviteUser(user)}>
|
||||
x
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
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>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{showAddButton ? (
|
||||
<PeopleDropdown
|
||||
selectedPeople={users}
|
||||
onSelect={updateInviteUsers}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{showInviteDesc ? (
|
||||
<>
|
||||
<div className="text-muted mb-3">{t('desc')}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline-primary"
|
||||
onClick={() => setEditing(true)}>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -5,6 +5,7 @@ import RelatedQuestions from './RelatedQuestions';
|
|||
import WriteAnswer from './WriteAnswer';
|
||||
import Alert from './Alert';
|
||||
import ContentLoader from './ContentLoader';
|
||||
import InviteToAnswer from './InviteToAnswer';
|
||||
|
||||
export {
|
||||
Question,
|
||||
|
@ -14,4 +15,5 @@ export {
|
|||
WriteAnswer,
|
||||
Alert,
|
||||
ContentLoader,
|
||||
InviteToAnswer,
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
WriteAnswer,
|
||||
Alert,
|
||||
ContentLoader,
|
||||
InviteToAnswer,
|
||||
} from './components';
|
||||
|
||||
import './index.scss';
|
||||
|
@ -197,6 +198,18 @@ const Index = () => {
|
|||
description: question?.description,
|
||||
keywords: question?.tags.map((_) => _.slug_name).join(','),
|
||||
});
|
||||
|
||||
const showInviteToAnswer = question?.id;
|
||||
let canInvitePeople = false;
|
||||
if (showInviteToAnswer && Array.isArray(question.extends_actions)) {
|
||||
const inviteAct = question.extends_actions.find((op) => {
|
||||
return op.action === 'invite_other_to_answer';
|
||||
});
|
||||
if (inviteAct) {
|
||||
canInvitePeople = true;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="questionDetailPage pt-4 mb-5">
|
||||
<Col className="flex-auto">
|
||||
|
@ -257,6 +270,12 @@ const Index = () => {
|
|||
<Col className="page-right-side mt-4 mt-xl-0">
|
||||
<CustomSidebar />
|
||||
<RelatedQuestions id={question?.id || ''} />
|
||||
{showInviteToAnswer ? (
|
||||
<InviteToAnswer
|
||||
questionId={question.id}
|
||||
readOnly={!canInvitePeople}
|
||||
/>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.inbox-nav {
|
||||
border-top: 1px solid rgba(0, 0, 0, .125);
|
||||
padding: .5rem 0;
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Row, Col, ButtonGroup, Button } from 'react-bootstrap';
|
||||
import { Row, Col, ButtonGroup, Button, Nav } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import {
|
||||
|
@ -14,6 +16,7 @@ import { floppyNavigation } from '@/utils';
|
|||
|
||||
import Inbox from './components/Inbox';
|
||||
import Achievements from './components/Achievements';
|
||||
import './index.scss';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
|
@ -21,13 +24,23 @@ const Notifications = () => {
|
|||
const [page, setPage] = useState(1);
|
||||
const [notificationData, setNotificationData] = useState<any>([]);
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'notifications' });
|
||||
const { type = 'inbox' } = useParams();
|
||||
const inboxTypeNavs = ['all', 'posts', 'invites', 'votes'];
|
||||
const { type = 'inbox', subType = inboxTypeNavs[0] } = useParams();
|
||||
|
||||
const { data, mutate } = useQueryNotifications({
|
||||
const queryParams: {
|
||||
type: string;
|
||||
inbox_type?: string;
|
||||
page: number;
|
||||
page_size: number;
|
||||
} = {
|
||||
type,
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
});
|
||||
};
|
||||
if (type === 'inbox') {
|
||||
queryParams.inbox_type = subType;
|
||||
}
|
||||
const { data, mutate } = useQueryNotifications(queryParams);
|
||||
|
||||
useEffect(() => {
|
||||
clearNotificationStatus(type);
|
||||
|
@ -104,10 +117,32 @@ const Notifications = () => {
|
|||
</Button>
|
||||
</div>
|
||||
{type === 'inbox' && (
|
||||
<Inbox
|
||||
data={notificationData}
|
||||
handleReadNotification={handleReadNotification}
|
||||
/>
|
||||
<>
|
||||
<Nav className="inbox-nav">
|
||||
{inboxTypeNavs.map((nav) => {
|
||||
const navLinkHref = `/users/notifications/inbox/${nav}`;
|
||||
const navLinkName = t(`inbox_type.${nav}`);
|
||||
return (
|
||||
<Nav.Item key={nav}>
|
||||
<Link
|
||||
to={navLinkHref}
|
||||
onClick={() => {
|
||||
setPage(1);
|
||||
}}
|
||||
className={classNames('nav-link', {
|
||||
disabled: nav === subType,
|
||||
})}>
|
||||
{navLinkName}
|
||||
</Link>
|
||||
</Nav.Item>
|
||||
);
|
||||
})}
|
||||
</Nav>
|
||||
<Inbox
|
||||
data={notificationData}
|
||||
handleReadNotification={handleReadNotification}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === 'achievement' && (
|
||||
<Achievements
|
||||
|
|
|
@ -163,7 +163,7 @@ const routes: RouteNode[] = [
|
|||
],
|
||||
},
|
||||
{
|
||||
path: 'users/notifications/:type',
|
||||
path: 'users/notifications/:type/:subType?',
|
||||
page: 'pages/Users/Notifications',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -53,3 +53,18 @@ export const useSimilarQuestion = (params: {
|
|||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInviteUser = (questionId: string) => {
|
||||
const apiUrl = '/answer/api/v1/question/invite';
|
||||
return request.get<Type.UserInfoBase[]>(apiUrl, {
|
||||
params: { id: questionId },
|
||||
});
|
||||
};
|
||||
|
||||
export const putInviteUser = (questionId: string, users: string[]) => {
|
||||
const apiUrl = '/answer/api/v1/question/invite';
|
||||
return request.put(apiUrl, {
|
||||
id: questionId,
|
||||
invite_user: users,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -11,3 +11,12 @@ export const useQueryContributeUsers = () => {
|
|||
staffs: Type.User[];
|
||||
}>(apiUrl, request.instance.get);
|
||||
};
|
||||
|
||||
export const userSearchByName = (name: string) => {
|
||||
const apiUrl = '/answer/api/v1/user/info/search';
|
||||
return request.get<Type.UserInfoBase[]>(apiUrl, {
|
||||
params: {
|
||||
username: name,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue