diff --git a/apiserver/plane/app/views/search/base.py b/apiserver/plane/app/views/search/base.py index 1f6754a9e7c..b98e2855f44 100644 --- a/apiserver/plane/app/views/search/base.py +++ b/apiserver/plane/app/views/search/base.py @@ -277,28 +277,14 @@ def get(self, request, slug): for field in fields: q |= Q(**{f"{field}__icontains": query}) - base_filters = Q( - q, - is_active=True, - workspace__slug=slug, - member__is_bot=False, - project_id=project_id, - role__gt=10, - ) - if issue_id: - issue_created_by = ( - Issue.objects.filter(id=issue_id) - .values_list("created_by_id", flat=True) - .first() - ) - # Add condition to include `issue_created_by` in the query - filters = Q(member_id=issue_created_by) | base_filters - else: - filters = base_filters - - # Query to fetch users users = ( - ProjectMember.objects.filter(filters) + ProjectMember.objects.filter( + q, + is_active=True, + workspace__slug=slug, + member__is_bot=False, + project_id=project_id, + ) .annotate( member__avatar_url=Case( When( @@ -318,14 +304,35 @@ def get(self, request, slug): ) ) .order_by("-created_at") - .values( - "member__avatar_url", - "member__display_name", - "member__id", - )[:count] ) - response_data["user_mention"] = list(users) + if issue_id: + issue_created_by = ( + Issue.objects.filter(id=issue_id) + .values_list("created_by_id", flat=True) + .first() + ) + users = ( + users.filter(Q(role__gt=10) | Q(member_id=issue_created_by)) + .distinct() + .values( + "member__avatar_url", + "member__display_name", + "member__id", + ) + ) + else: + users = ( + users.filter(Q(role__gt=10)) + .distinct() + .values( + "member__avatar_url", + "member__display_name", + "member__id", + ) + ) + + response_data["user_mention"] = list(users[:count]) elif query_type == "project": fields = ["name", "identifier"] diff --git a/packages/types/src/search.d.ts b/packages/types/src/search.d.ts index 41f6a102130..41138a46eb8 100644 --- a/packages/types/src/search.d.ts +++ b/packages/types/src/search.d.ts @@ -74,4 +74,5 @@ export type TSearchEntityRequestPayload = { query_type: TSearchEntities[]; query: string; team_id?: string; + issue_id?: string; }; diff --git a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx index 84ac73b7550..f9eee044ae4 100644 --- a/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx +++ b/web/core/components/command-palette/actions/issue-actions/change-assignee.tsx @@ -33,31 +33,34 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { } = useMember(); const options = - projectMemberIds?.map((userId) => { - const memberDetails = getProjectMemberDetails(userId); + projectMemberIds + ?.map((userId) => { + if (!projectId) return; + const memberDetails = getProjectMemberDetails(userId, projectId.toString()); - return { - value: `${memberDetails?.member?.id}`, - query: `${memberDetails?.member?.display_name}`, - content: ( - <> -
- - {memberDetails?.member?.display_name} -
- {issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && ( -
- + return { + value: `${memberDetails?.member?.id}`, + query: `${memberDetails?.member?.display_name}`, + content: ( + <> +
+ + {memberDetails?.member?.display_name}
- )} - - ), - }; - }) ?? []; + {issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && ( +
+ +
+ )} + + ), + }; + }) + .filter((o) => o !== undefined) ?? []; const handleUpdateIssue = async (formData: Partial) => { if (!workspaceSlug || !projectId || !issue) return; @@ -80,15 +83,18 @@ export const ChangeIssueAssignee: React.FC = observer((props) => { return ( <> - {options.map((option) => ( - handleIssueAssignees(option.value)} - className="focus:outline-none" - > - {option.content} - - ))} + {options.map( + (option) => + option && ( + handleIssueAssignees(option.value)} + className="focus:outline-none" + > + {option.content} + + ) + )} ); }); diff --git a/web/core/components/dropdowns/member/member-options.tsx b/web/core/components/dropdowns/member/member-options.tsx index 81cf1945609..1925aa4a7a8 100644 --- a/web/core/components/dropdowns/member/member-options.tsx +++ b/web/core/components/dropdowns/member/member-options.tsx @@ -17,6 +17,7 @@ import { getFileURL } from "@/helpers/file.helper"; // hooks import { useUser, useMember } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; +import { EUserPermissions } from "@/plane-web/constants"; interface Props { className?: string; @@ -39,7 +40,7 @@ export const MemberOptions: React.FC = observer((props: Props) => { const { workspaceSlug } = useParams(); const { getUserDetails, - project: { getProjectMemberIds, fetchProjectMembers }, + project: { getProjectMemberIds, fetchProjectMembers, getProjectMemberDetails }, workspace: { workspaceMemberIds }, } = useMember(); const { data: currentUser } = useUser(); @@ -78,23 +79,32 @@ export const MemberOptions: React.FC = observer((props: Props) => { } }; - const options = memberIds?.map((userId) => { - const userDetails = getUserDetails(userId); + const options = memberIds + ?.map((userId) => { + const userDetails = getUserDetails(userId); + if (projectId) { + const role = getProjectMemberDetails(userId, projectId)?.role; + const isGuest = role === EUserPermissions.GUEST; + if (isGuest) return; + } - return { - value: userId, - query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, - content: ( -
- - {currentUser?.id === userId ? t("you") : userDetails?.display_name} -
- ), - }; - }); + return { + value: userId, + query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`, + content: ( +
+ + + {currentUser?.id === userId ? t("you") : userDetails?.display_name} + +
+ ), + }; + }) + .filter((o) => !!o); const filteredOptions = - query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase())); + query === "" ? options : options?.filter((o) => o?.query.toLowerCase().includes(query.toLowerCase())); return createPortal( @@ -125,24 +135,27 @@ export const MemberOptions: React.FC = observer((props: Props) => {
{filteredOptions ? ( filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ - active ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) + filteredOptions.map( + (option) => + option && ( + + `flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + ) + ) ) : (

{t("no_matching_results")}

) diff --git a/web/core/components/editor/embeds/mentions/user.tsx b/web/core/components/editor/embeds/mentions/user.tsx index 20cfeb2baa5..76b8a0f4967 100644 --- a/web/core/components/editor/embeds/mentions/user.tsx +++ b/web/core/components/editor/embeds/mentions/user.tsx @@ -19,6 +19,8 @@ type Props = { export const EditorUserMention: React.FC = observer((props) => { const { id } = props; + // router + const { projectId } = useParams(); // states const [popperElement, setPopperElement] = useState(null); const [referenceElement, setReferenceElement] = useState(null); @@ -44,7 +46,7 @@ export const EditorUserMention: React.FC = observer((props) => { }); // derived values const userDetails = getUserDetails(id); - const roleDetails = getProjectMemberDetails(id)?.role; + const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null; const profileLink = `/${workspaceSlug}/profile/${id}`; if (!userDetails) { diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index da3ac44923c..afaff3d7ea6 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -30,6 +30,7 @@ interface LiteTextEditorWrapperProps isSubmitting?: boolean; showToolbarInitially?: boolean; uploadFile: (file: File) => Promise; + issue_id?: string; } export const LiteTextEditor = React.forwardRef((props, ref) => { @@ -38,6 +39,7 @@ export const LiteTextEditor = React.forwardRef = observer((p await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", { ...payload, project_id: projectId?.toString() ?? "", + issue_id: issueId?.toString(), }) } containerClassName={containerClassName} diff --git a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 49747d2b3a1..205942d8fa1 100644 --- a/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -44,6 +44,7 @@ export const IssueActivityCommentRoot: FC = observer( activityComment.activity_type === "COMMENT" ? ( = observer((props) => { const { workspaceSlug, projectId, + issueId, commentId, activityOperations, ends, @@ -144,6 +146,7 @@ export const IssueCommentCard: FC = observer((props) => { = (props) => { id={"add_comment_" + issueId} value={"

"} projectId={projectId} + issue_id={issueId} workspaceSlug={workspaceSlug} onEnterKeyPress={(e) => { if (!isEmpty && !isSubmitting) { diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx index 29272bb91eb..7b481af355b 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -36,6 +36,7 @@ export const IssueCommentRoot: FC = observer((props) => { commentIds.map((commentId, index) => ( { + // router + const { projectId } = useParams(); // states const [inviteModal, setInviteModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -24,7 +27,7 @@ export const ProjectMemberList: React.FC = observer(() => { } = useMember(); const { allowPermissions } = useUserPermissions(); const searchedMembers = (projectMemberIds ?? []).filter((userId) => { - const memberDetails = getProjectMemberDetails(userId); + const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null; if (!memberDetails?.member) return false; @@ -33,7 +36,9 @@ export const ProjectMemberList: React.FC = observer(() => { return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase()); }); - const memberDetails = searchedMembers?.map((memberId) => getProjectMemberDetails(memberId)); + const memberDetails = searchedMembers?.map((memberId) => + projectId ? getProjectMemberDetails(memberId, projectId.toString()) : null + ); const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); diff --git a/web/core/components/project/member-select.tsx b/web/core/components/project/member-select.tsx index 706481c237e..31493a971e5 100644 --- a/web/core/components/project/member-select.tsx +++ b/web/core/components/project/member-select.tsx @@ -2,6 +2,7 @@ import React from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; import { Ban } from "lucide-react"; // plane ui import { Avatar, CustomSearchSelect } from "@plane/ui"; @@ -9,6 +10,7 @@ import { Avatar, CustomSearchSelect } from "@plane/ui"; import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember } from "@/hooks/store"; +import { EUserPermissions } from "@/plane-web/constants"; type Props = { value: any; @@ -18,6 +20,8 @@ type Props = { export const MemberSelect: React.FC = observer((props) => { const { value, onChange, isDisabled = false } = props; + // router + const { projectId } = useParams(); // store hooks const { project: { projectMemberIds, getProjectMemberDetails }, @@ -25,9 +29,11 @@ export const MemberSelect: React.FC = observer((props) => { const options = projectMemberIds ?.map((userId) => { - const memberDetails = getProjectMemberDetails(userId); + const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null; if (!memberDetails?.member) return; + const isGuest = memberDetails.role === EUserPermissions.GUEST; + if (isGuest) return; return { value: `${memberDetails?.member.id}`, @@ -47,7 +53,7 @@ export const MemberSelect: React.FC = observer((props) => { content: React.JSX.Element; }[] | undefined; - const selectedOption = getProjectMemberDetails(value); + const selectedOption = projectId ? getProjectMemberDetails(value, projectId.toString()) : null; return ( = observer((props) => Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST ); const isCurrentUserProjectMember = currentUser - ? getProjectMemberDetails(currentUser.id)?.role === EUserPermissions.MEMBER + ? getProjectMemberDetails(currentUser.id, projectId)?.role === EUserPermissions.MEMBER : false; const isRoleNonEditable = isCurrentUser || (isProjectAdminOrGuest && !isWorkspaceMember) || isCurrentUserProjectMember; diff --git a/web/core/store/member/project-member.store.ts b/web/core/store/member/project-member.store.ts index af6f74df0d5..22a20ba7c9f 100644 --- a/web/core/store/member/project-member.store.ts +++ b/web/core/store/member/project-member.store.ts @@ -36,7 +36,7 @@ export interface IProjectMemberStore { // computed projectMemberIds: string[] | null; // computed actions - getProjectMemberDetails: (userId: string) => IProjectMemberDetails | null; + getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null; getProjectMemberIds: (projectId: string) => string[] | null; // fetch actions fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise; @@ -110,12 +110,10 @@ export class ProjectMemberStore implements IProjectMemberStore { * @description get the details of a project member * @param userId */ - getProjectMemberDetails = computedFn((userId: string) => { - const projectId = this.routerStore.projectId; - if (!projectId) return null; + getProjectMemberDetails = computedFn((userId: string, projectId: string) => { const projectMember = this.projectMemberMap?.[projectId]?.[userId]; if (!projectMember) return null; - + console.log({ projectMember }); const memberDetails: IProjectMemberDetails = { id: projectMember.id, role: projectMember.role, @@ -183,7 +181,7 @@ export class ProjectMemberStore implements IProjectMemberStore { * @param data */ updateMember = async (workspaceSlug: string, projectId: string, userId: string, data: { role: EUserPermissions }) => { - const memberDetails = this.getProjectMemberDetails(userId); + const memberDetails = this.getProjectMemberDetails(userId, projectId); if (!memberDetails) throw new Error("Member not found"); // original data to revert back in case of error const originalProjectMemberData = this.projectMemberMap?.[projectId]?.[userId]; @@ -214,7 +212,7 @@ export class ProjectMemberStore implements IProjectMemberStore { * @param userId */ removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => { - const memberDetails = this.getProjectMemberDetails(userId); + const memberDetails = this.getProjectMemberDetails(userId, projectId); if (!memberDetails) throw new Error("Member not found"); await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => { runInAction(() => {