From 05613379420e28d60eff877d319a7c71652bb3e8 Mon Sep 17 00:00:00 2001 From: haitaoo Date: Wed, 24 May 2023 15:11:14 +0800 Subject: [PATCH] feat(Question): Implementing the invite to answer function --- i18n/en_US.yaml | 7 + ui/src/common/interface.ts | 2 +- .../InviteToAnswer/PeopleDropdown.tsx | 95 +++++++++++ .../components/InviteToAnswer/index.tsx | 154 ++++++++++++++++++ .../Questions/Detail/components/index.tsx | 2 + ui/src/pages/Questions/Detail/index.tsx | 19 +++ ui/src/services/client/question.ts | 15 ++ ui/src/services/client/user.ts | 9 + 8 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx create mode 100644 ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index c7a40c8b..be9a2c1e 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -896,6 +896,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 @@ -962,6 +968,7 @@ ui: btns: confirm: Confirm cancel: Cancel + edit: Edit save: Save delete: Delete login: Log in diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index a5310d1e..1b764353 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -122,7 +122,7 @@ export interface UserInfoBase { */ status?: string; /** roles */ - role_id: RoleId; + role_id?: RoleId; } export interface UserInfoRes extends UserInfoBase { diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx new file mode 100644 index 00000000..e108359d --- /dev/null +++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/PeopleDropdown.tsx @@ -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 = ({ selectedPeople = [], onSelect }) => { + const { user: currentUser } = loggedUserInfoStore(); + const { t } = useTranslation('translation', { + keyPrefix: 'invite_to_answer', + }); + const [toggleState, setToggleState] = useState(false); + const [peopleList, setPeopleList] = useState([]); + + 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 ( + + + {t('add')} + + + + + + + + {peopleList.map((p, idx) => { + return ( + +
+ + {p.display_name} + @{p.username} +
+
+ ); + })} +
+
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx new file mode 100644 index 00000000..0855ba10 --- /dev/null +++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx @@ -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 = ({ 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(); + + 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 ( + + + {t('title')} + {!readOnly && editing ? ( + + ) : null} + {!readOnly && !editing ? ( + + ) : null} + + +
+ {users?.map((user) => { + if (editing) { + return ( + + ); + } + return ( + + + {user.display_name} + + ); + })} + {showAddButton ? ( + + ) : null} +
+ {showInviteDesc ? ( + <> +
{t('desc')}
+ + + ) : null} +
+
+ ); +}; + +export default memo(Index); diff --git a/ui/src/pages/Questions/Detail/components/index.tsx b/ui/src/pages/Questions/Detail/components/index.tsx index 02ac9678..6cf44028 100644 --- a/ui/src/pages/Questions/Detail/components/index.tsx +++ b/ui/src/pages/Questions/Detail/components/index.tsx @@ -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, }; diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index d01cb525..7a17dff6 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -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 ( @@ -257,6 +270,12 @@ const Index = () => { + {showInviteToAnswer ? ( + + ) : null} ); diff --git a/ui/src/services/client/question.ts b/ui/src/services/client/question.ts index f62aa88c..db66462a 100644 --- a/ui/src/services/client/question.ts +++ b/ui/src/services/client/question.ts @@ -53,3 +53,18 @@ export const useSimilarQuestion = (params: { error, }; }; + +export const getInviteUser = (questionId: string) => { + const apiUrl = '/answer/api/v1/question/invite'; + return request.get(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, + }); +}; diff --git a/ui/src/services/client/user.ts b/ui/src/services/client/user.ts index 79796c98..340f0973 100644 --- a/ui/src/services/client/user.ts +++ b/ui/src/services/client/user.ts @@ -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(apiUrl, { + params: { + username: name, + }, + }); +};