[WEB-2711]fix: guest mentions and assignees (#6315)
* chore: issue search filter * * fix: restricting guest user from assignees and mentions --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
d26fb8ce02
commit
625cbf872b
15 changed files with 153 additions and 105 deletions
|
|
@ -277,28 +277,14 @@ class SearchEndpoint(BaseAPIView):
|
||||||
for field in fields:
|
for field in fields:
|
||||||
q |= Q(**{f"{field}__icontains": query})
|
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 = (
|
users = (
|
||||||
ProjectMember.objects.filter(filters)
|
ProjectMember.objects.filter(
|
||||||
|
q,
|
||||||
|
is_active=True,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
member__avatar_url=Case(
|
member__avatar_url=Case(
|
||||||
When(
|
When(
|
||||||
|
|
@ -318,14 +304,35 @@ class SearchEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by("-created_at")
|
.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":
|
elif query_type == "project":
|
||||||
fields = ["name", "identifier"]
|
fields = ["name", "identifier"]
|
||||||
|
|
|
||||||
1
packages/types/src/search.d.ts
vendored
1
packages/types/src/search.d.ts
vendored
|
|
@ -74,4 +74,5 @@ export type TSearchEntityRequestPayload = {
|
||||||
query_type: TSearchEntities[];
|
query_type: TSearchEntities[];
|
||||||
query: string;
|
query: string;
|
||||||
team_id?: string;
|
team_id?: string;
|
||||||
|
issue_id?: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,31 +33,34 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||||
} = useMember();
|
} = useMember();
|
||||||
|
|
||||||
const options =
|
const options =
|
||||||
projectMemberIds?.map((userId) => {
|
projectMemberIds
|
||||||
const memberDetails = getProjectMemberDetails(userId);
|
?.map((userId) => {
|
||||||
|
if (!projectId) return;
|
||||||
|
const memberDetails = getProjectMemberDetails(userId, projectId.toString());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: `${memberDetails?.member?.id}`,
|
value: `${memberDetails?.member?.id}`,
|
||||||
query: `${memberDetails?.member?.display_name}`,
|
query: `${memberDetails?.member?.display_name}`,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
name={memberDetails?.member?.display_name}
|
name={memberDetails?.member?.display_name}
|
||||||
src={getFileURL(memberDetails?.member?.avatar_url ?? "")}
|
src={getFileURL(memberDetails?.member?.avatar_url ?? "")}
|
||||||
showTooltip={false}
|
showTooltip={false}
|
||||||
/>
|
/>
|
||||||
{memberDetails?.member?.display_name}
|
{memberDetails?.member?.display_name}
|
||||||
</div>
|
|
||||||
{issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && (
|
|
||||||
<div>
|
|
||||||
<Check className="h-3 w-3" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{issue.assignee_ids.includes(memberDetails?.member?.id ?? "") && (
|
||||||
</>
|
<div>
|
||||||
),
|
<Check className="h-3 w-3" />
|
||||||
};
|
</div>
|
||||||
}) ?? [];
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((o) => o !== undefined) ?? [];
|
||||||
|
|
||||||
const handleUpdateIssue = async (formData: Partial<TIssue>) => {
|
const handleUpdateIssue = async (formData: Partial<TIssue>) => {
|
||||||
if (!workspaceSlug || !projectId || !issue) return;
|
if (!workspaceSlug || !projectId || !issue) return;
|
||||||
|
|
@ -80,15 +83,18 @@ export const ChangeIssueAssignee: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{options.map((option) => (
|
{options.map(
|
||||||
<Command.Item
|
(option) =>
|
||||||
key={option.value}
|
option && (
|
||||||
onSelect={() => handleIssueAssignees(option.value)}
|
<Command.Item
|
||||||
className="focus:outline-none"
|
key={option.value}
|
||||||
>
|
onSelect={() => handleIssueAssignees(option.value)}
|
||||||
{option.content}
|
className="focus:outline-none"
|
||||||
</Command.Item>
|
>
|
||||||
))}
|
{option.content}
|
||||||
|
</Command.Item>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { getFileURL } from "@/helpers/file.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser, useMember } from "@/hooks/store";
|
import { useUser, useMember } from "@/hooks/store";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
|
import { EUserPermissions } from "@/plane-web/constants";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -39,7 +40,7 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||||
const { workspaceSlug } = useParams();
|
const { workspaceSlug } = useParams();
|
||||||
const {
|
const {
|
||||||
getUserDetails,
|
getUserDetails,
|
||||||
project: { getProjectMemberIds, fetchProjectMembers },
|
project: { getProjectMemberIds, fetchProjectMembers, getProjectMemberDetails },
|
||||||
workspace: { workspaceMemberIds },
|
workspace: { workspaceMemberIds },
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { data: currentUser } = useUser();
|
const { data: currentUser } = useUser();
|
||||||
|
|
@ -78,23 +79,32 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = memberIds?.map((userId) => {
|
const options = memberIds
|
||||||
const userDetails = getUserDetails(userId);
|
?.map((userId) => {
|
||||||
|
const userDetails = getUserDetails(userId);
|
||||||
|
if (projectId) {
|
||||||
|
const role = getProjectMemberDetails(userId, projectId)?.role;
|
||||||
|
const isGuest = role === EUserPermissions.GUEST;
|
||||||
|
if (isGuest) return;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: userId,
|
value: userId,
|
||||||
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
|
query: `${userDetails?.display_name} ${userDetails?.first_name} ${userDetails?.last_name}`,
|
||||||
content: (
|
content: (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
<Avatar name={userDetails?.display_name} src={getFileURL(userDetails?.avatar_url ?? "")} />
|
||||||
<span className="flex-grow truncate">{currentUser?.id === userId ? t("you") : userDetails?.display_name}</span>
|
<span className="flex-grow truncate">
|
||||||
</div>
|
{currentUser?.id === userId ? t("you") : userDetails?.display_name}
|
||||||
),
|
</span>
|
||||||
};
|
</div>
|
||||||
});
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((o) => !!o);
|
||||||
|
|
||||||
const filteredOptions =
|
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(
|
return createPortal(
|
||||||
<Combobox.Options data-prevent-outside-click static>
|
<Combobox.Options data-prevent-outside-click static>
|
||||||
|
|
@ -125,24 +135,27 @@ export const MemberOptions: React.FC<Props> = observer((props: Props) => {
|
||||||
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
|
||||||
{filteredOptions ? (
|
{filteredOptions ? (
|
||||||
filteredOptions.length > 0 ? (
|
filteredOptions.length > 0 ? (
|
||||||
filteredOptions.map((option) => (
|
filteredOptions.map(
|
||||||
<Combobox.Option
|
(option) =>
|
||||||
key={option.value}
|
option && (
|
||||||
value={option.value}
|
<Combobox.Option
|
||||||
className={({ active, selected }) =>
|
key={option.value}
|
||||||
`flex w-full cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 ${
|
value={option.value}
|
||||||
active ? "bg-custom-background-80" : ""
|
className={({ active, selected }) =>
|
||||||
} ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
|
`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 }) => (
|
}
|
||||||
<>
|
>
|
||||||
<span className="flex-grow truncate">{option.content}</span>
|
{({ selected }) => (
|
||||||
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
<>
|
||||||
</>
|
<span className="flex-grow truncate">{option.content}</span>
|
||||||
)}
|
{selected && <Check className="h-3.5 w-3.5 flex-shrink-0" />}
|
||||||
</Combobox.Option>
|
</>
|
||||||
))
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<p className="px-1.5 py-1 italic text-custom-text-400">{t("no_matching_results")}</p>
|
<p className="px-1.5 py-1 italic text-custom-text-400">{t("no_matching_results")}</p>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ type Props = {
|
||||||
|
|
||||||
export const EditorUserMention: React.FC<Props> = observer((props) => {
|
export const EditorUserMention: React.FC<Props> = observer((props) => {
|
||||||
const { id } = props;
|
const { id } = props;
|
||||||
|
// router
|
||||||
|
const { projectId } = useParams();
|
||||||
// states
|
// states
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
const [referenceElement, setReferenceElement] = useState<HTMLAnchorElement | null>(null);
|
const [referenceElement, setReferenceElement] = useState<HTMLAnchorElement | null>(null);
|
||||||
|
|
@ -44,7 +46,7 @@ export const EditorUserMention: React.FC<Props> = observer((props) => {
|
||||||
});
|
});
|
||||||
// derived values
|
// derived values
|
||||||
const userDetails = getUserDetails(id);
|
const userDetails = getUserDetails(id);
|
||||||
const roleDetails = getProjectMemberDetails(id)?.role;
|
const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null;
|
||||||
const profileLink = `/${workspaceSlug}/profile/${id}`;
|
const profileLink = `/${workspaceSlug}/profile/${id}`;
|
||||||
|
|
||||||
if (!userDetails) {
|
if (!userDetails) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ interface LiteTextEditorWrapperProps
|
||||||
isSubmitting?: boolean;
|
isSubmitting?: boolean;
|
||||||
showToolbarInitially?: boolean;
|
showToolbarInitially?: boolean;
|
||||||
uploadFile: (file: File) => Promise<string>;
|
uploadFile: (file: File) => Promise<string>;
|
||||||
|
issue_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
|
||||||
|
|
@ -38,6 +39,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
projectId,
|
projectId,
|
||||||
|
issue_id,
|
||||||
accessSpecifier,
|
accessSpecifier,
|
||||||
handleAccessChange,
|
handleAccessChange,
|
||||||
showAccessSpecifier = false,
|
showAccessSpecifier = false,
|
||||||
|
|
@ -58,6 +60,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||||
...payload,
|
...payload,
|
||||||
project_id: projectId?.toString() ?? "",
|
project_id: projectId?.toString() ?? "",
|
||||||
|
issue_id: issue_id,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
// file size
|
// file size
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ export const IssueDescriptionInput: FC<IssueDescriptionInputProps> = observer((p
|
||||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||||
...payload,
|
...payload,
|
||||||
project_id: projectId?.toString() ?? "",
|
project_id: projectId?.toString() ?? "",
|
||||||
|
issue_id: issueId?.toString(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
containerClassName={containerClassName}
|
containerClassName={containerClassName}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ export const IssueActivityCommentRoot: FC<TIssueActivityCommentRoot> = observer(
|
||||||
activityComment.activity_type === "COMMENT" ? (
|
activityComment.activity_type === "COMMENT" ? (
|
||||||
<IssueCommentCard
|
<IssueCommentCard
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
key={activityComment.id}
|
key={activityComment.id}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
commentId={activityComment.id}
|
commentId={activityComment.id}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { IssueCommentBlock } from "./comment-block";
|
||||||
|
|
||||||
type TIssueCommentCard = {
|
type TIssueCommentCard = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
commentId: string;
|
commentId: string;
|
||||||
activityOperations: TActivityOperations;
|
activityOperations: TActivityOperations;
|
||||||
|
|
@ -34,6 +35,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
workspaceSlug,
|
workspaceSlug,
|
||||||
projectId,
|
projectId,
|
||||||
|
issueId,
|
||||||
commentId,
|
commentId,
|
||||||
activityOperations,
|
activityOperations,
|
||||||
ends,
|
ends,
|
||||||
|
|
@ -144,6 +146,7 @@ export const IssueCommentCard: FC<TIssueCommentCard> = observer((props) => {
|
||||||
<LiteTextEditor
|
<LiteTextEditor
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
issue_id={issueId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
id={comment.id}
|
id={comment.id}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ export const IssueCommentCreate: FC<TIssueCommentCreate> = (props) => {
|
||||||
id={"add_comment_" + issueId}
|
id={"add_comment_" + issueId}
|
||||||
value={"<p></p>"}
|
value={"<p></p>"}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
issue_id={issueId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
onEnterKeyPress={(e) => {
|
onEnterKeyPress={(e) => {
|
||||||
if (!isEmpty && !isSubmitting) {
|
if (!isEmpty && !isSubmitting) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export const IssueCommentRoot: FC<TIssueCommentRoot> = observer((props) => {
|
||||||
commentIds.map((commentId, index) => (
|
commentIds.map((commentId, index) => (
|
||||||
<IssueCommentCard
|
<IssueCommentCard
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
key={commentId}
|
key={commentId}
|
||||||
workspaceSlug={workspaceSlug}
|
workspaceSlug={workspaceSlug}
|
||||||
commentId={commentId}
|
commentId={commentId}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
// components
|
// components
|
||||||
|
|
@ -14,6 +15,8 @@ import { useEventTracker, useMember, useUserPermissions } from "@/hooks/store";
|
||||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||||
|
|
||||||
export const ProjectMemberList: React.FC = observer(() => {
|
export const ProjectMemberList: React.FC = observer(() => {
|
||||||
|
// router
|
||||||
|
const { projectId } = useParams();
|
||||||
// states
|
// states
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
@ -24,7 +27,7 @@ export const ProjectMemberList: React.FC = observer(() => {
|
||||||
} = useMember();
|
} = useMember();
|
||||||
const { allowPermissions } = useUserPermissions();
|
const { allowPermissions } = useUserPermissions();
|
||||||
const searchedMembers = (projectMemberIds ?? []).filter((userId) => {
|
const searchedMembers = (projectMemberIds ?? []).filter((userId) => {
|
||||||
const memberDetails = getProjectMemberDetails(userId);
|
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
||||||
|
|
||||||
if (!memberDetails?.member) return false;
|
if (!memberDetails?.member) return false;
|
||||||
|
|
||||||
|
|
@ -33,7 +36,9 @@ export const ProjectMemberList: React.FC = observer(() => {
|
||||||
|
|
||||||
return displayName?.includes(searchQuery.toLowerCase()) || fullName.includes(searchQuery.toLowerCase());
|
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);
|
const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
import { Ban } from "lucide-react";
|
import { Ban } from "lucide-react";
|
||||||
// plane ui
|
// plane ui
|
||||||
import { Avatar, CustomSearchSelect } from "@plane/ui";
|
import { Avatar, CustomSearchSelect } from "@plane/ui";
|
||||||
|
|
@ -9,6 +10,7 @@ import { Avatar, CustomSearchSelect } from "@plane/ui";
|
||||||
import { getFileURL } from "@/helpers/file.helper";
|
import { getFileURL } from "@/helpers/file.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember } from "@/hooks/store";
|
import { useMember } from "@/hooks/store";
|
||||||
|
import { EUserPermissions } from "@/plane-web/constants";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: any;
|
value: any;
|
||||||
|
|
@ -18,6 +20,8 @@ type Props = {
|
||||||
|
|
||||||
export const MemberSelect: React.FC<Props> = observer((props) => {
|
export const MemberSelect: React.FC<Props> = observer((props) => {
|
||||||
const { value, onChange, isDisabled = false } = props;
|
const { value, onChange, isDisabled = false } = props;
|
||||||
|
// router
|
||||||
|
const { projectId } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
project: { projectMemberIds, getProjectMemberDetails },
|
project: { projectMemberIds, getProjectMemberDetails },
|
||||||
|
|
@ -25,9 +29,11 @@ export const MemberSelect: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
const options = projectMemberIds
|
const options = projectMemberIds
|
||||||
?.map((userId) => {
|
?.map((userId) => {
|
||||||
const memberDetails = getProjectMemberDetails(userId);
|
const memberDetails = projectId ? getProjectMemberDetails(userId, projectId.toString()) : null;
|
||||||
|
|
||||||
if (!memberDetails?.member) return;
|
if (!memberDetails?.member) return;
|
||||||
|
const isGuest = memberDetails.role === EUserPermissions.GUEST;
|
||||||
|
if (isGuest) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: `${memberDetails?.member.id}`,
|
value: `${memberDetails?.member.id}`,
|
||||||
|
|
@ -47,7 +53,7 @@ export const MemberSelect: React.FC<Props> = observer((props) => {
|
||||||
content: React.JSX.Element;
|
content: React.JSX.Element;
|
||||||
}[]
|
}[]
|
||||||
| undefined;
|
| undefined;
|
||||||
const selectedOption = getProjectMemberDetails(value);
|
const selectedOption = projectId ? getProjectMemberDetails(value, projectId.toString()) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomSearchSelect
|
<CustomSearchSelect
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ export const AccountTypeColumn: React.FC<AccountTypeProps> = observer((props) =>
|
||||||
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
|
Number(getWorkspaceMemberDetails(rowData.member.id)?.role) ?? EUserPermissions.GUEST
|
||||||
);
|
);
|
||||||
const isCurrentUserProjectMember = currentUser
|
const isCurrentUserProjectMember = currentUser
|
||||||
? getProjectMemberDetails(currentUser.id)?.role === EUserPermissions.MEMBER
|
? getProjectMemberDetails(currentUser.id, projectId)?.role === EUserPermissions.MEMBER
|
||||||
: false;
|
: false;
|
||||||
const isRoleNonEditable =
|
const isRoleNonEditable =
|
||||||
isCurrentUser || (isProjectAdminOrGuest && !isWorkspaceMember) || isCurrentUserProjectMember;
|
isCurrentUser || (isProjectAdminOrGuest && !isWorkspaceMember) || isCurrentUserProjectMember;
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export interface IProjectMemberStore {
|
||||||
// computed
|
// computed
|
||||||
projectMemberIds: string[] | null;
|
projectMemberIds: string[] | null;
|
||||||
// computed actions
|
// computed actions
|
||||||
getProjectMemberDetails: (userId: string) => IProjectMemberDetails | null;
|
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||||
getProjectMemberIds: (projectId: string) => string[] | null;
|
getProjectMemberIds: (projectId: string) => string[] | null;
|
||||||
// fetch actions
|
// fetch actions
|
||||||
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<IProjectMembership[]>;
|
fetchProjectMembers: (workspaceSlug: string, projectId: string) => Promise<IProjectMembership[]>;
|
||||||
|
|
@ -110,12 +110,10 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
* @description get the details of a project member
|
* @description get the details of a project member
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
getProjectMemberDetails = computedFn((userId: string) => {
|
getProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
||||||
const projectId = this.routerStore.projectId;
|
|
||||||
if (!projectId) return null;
|
|
||||||
const projectMember = this.projectMemberMap?.[projectId]?.[userId];
|
const projectMember = this.projectMemberMap?.[projectId]?.[userId];
|
||||||
if (!projectMember) return null;
|
if (!projectMember) return null;
|
||||||
|
console.log({ projectMember });
|
||||||
const memberDetails: IProjectMemberDetails = {
|
const memberDetails: IProjectMemberDetails = {
|
||||||
id: projectMember.id,
|
id: projectMember.id,
|
||||||
role: projectMember.role,
|
role: projectMember.role,
|
||||||
|
|
@ -183,7 +181,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
* @param data
|
* @param data
|
||||||
*/
|
*/
|
||||||
updateMember = async (workspaceSlug: string, projectId: string, userId: string, data: { role: EUserPermissions }) => {
|
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");
|
if (!memberDetails) throw new Error("Member not found");
|
||||||
// original data to revert back in case of error
|
// original data to revert back in case of error
|
||||||
const originalProjectMemberData = this.projectMemberMap?.[projectId]?.[userId];
|
const originalProjectMemberData = this.projectMemberMap?.[projectId]?.[userId];
|
||||||
|
|
@ -214,7 +212,7 @@ export class ProjectMemberStore implements IProjectMemberStore {
|
||||||
* @param userId
|
* @param userId
|
||||||
*/
|
*/
|
||||||
removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => {
|
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");
|
if (!memberDetails) throw new Error("Member not found");
|
||||||
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => {
|
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue