mirror of https://gitee.com/answerdev/answer.git
feat(Question): Implementing the invite to answer function
This commit is contained in:
parent
2cea40c809
commit
0561337942
|
@ -896,6 +896,12 @@ ui:
|
||||||
title: Related Questions
|
title: Related Questions
|
||||||
btn: Add question
|
btn: Add question
|
||||||
answers: answers
|
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:
|
question_detail:
|
||||||
action: Action
|
action: Action
|
||||||
Asked: Asked
|
Asked: Asked
|
||||||
|
@ -962,6 +968,7 @@ ui:
|
||||||
btns:
|
btns:
|
||||||
confirm: Confirm
|
confirm: Confirm
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
|
edit: Edit
|
||||||
save: Save
|
save: Save
|
||||||
delete: Delete
|
delete: Delete
|
||||||
login: Log in
|
login: Log in
|
||||||
|
|
|
@ -122,7 +122,7 @@ export interface UserInfoBase {
|
||||||
*/
|
*/
|
||||||
status?: string;
|
status?: string;
|
||||||
/** roles */
|
/** roles */
|
||||||
role_id: RoleId;
|
role_id?: RoleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserInfoRes extends UserInfoBase {
|
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 WriteAnswer from './WriteAnswer';
|
||||||
import Alert from './Alert';
|
import Alert from './Alert';
|
||||||
import ContentLoader from './ContentLoader';
|
import ContentLoader from './ContentLoader';
|
||||||
|
import InviteToAnswer from './InviteToAnswer';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Question,
|
Question,
|
||||||
|
@ -14,4 +15,5 @@ export {
|
||||||
WriteAnswer,
|
WriteAnswer,
|
||||||
Alert,
|
Alert,
|
||||||
ContentLoader,
|
ContentLoader,
|
||||||
|
InviteToAnswer,
|
||||||
};
|
};
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {
|
||||||
WriteAnswer,
|
WriteAnswer,
|
||||||
Alert,
|
Alert,
|
||||||
ContentLoader,
|
ContentLoader,
|
||||||
|
InviteToAnswer,
|
||||||
} from './components';
|
} from './components';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
@ -197,6 +198,18 @@ const Index = () => {
|
||||||
description: question?.description,
|
description: question?.description,
|
||||||
keywords: question?.tags.map((_) => _.slug_name).join(','),
|
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 (
|
return (
|
||||||
<Row className="questionDetailPage pt-4 mb-5">
|
<Row className="questionDetailPage pt-4 mb-5">
|
||||||
<Col className="flex-auto">
|
<Col className="flex-auto">
|
||||||
|
@ -257,6 +270,12 @@ const Index = () => {
|
||||||
<Col className="page-right-side mt-4 mt-xl-0">
|
<Col className="page-right-side mt-4 mt-xl-0">
|
||||||
<CustomSidebar />
|
<CustomSidebar />
|
||||||
<RelatedQuestions id={question?.id || ''} />
|
<RelatedQuestions id={question?.id || ''} />
|
||||||
|
{showInviteToAnswer ? (
|
||||||
|
<InviteToAnswer
|
||||||
|
questionId={question.id}
|
||||||
|
readOnly={!canInvitePeople}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,3 +53,18 @@ export const useSimilarQuestion = (params: {
|
||||||
error,
|
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[];
|
staffs: Type.User[];
|
||||||
}>(apiUrl, request.instance.get);
|
}>(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