diff --git a/apiserver/plane/app/views/project/member.py b/apiserver/plane/app/views/project/member.py index d8d99fbf0e..26399be245 100644 --- a/apiserver/plane/app/views/project/member.py +++ b/apiserver/plane/app/views/project/member.py @@ -3,6 +3,7 @@ from rest_framework.response import Response from rest_framework import status import json + # Django imports from django.utils import timezone from django.core.serializers.json import DjangoJSONEncoder @@ -21,13 +22,14 @@ from plane.app.permissions import ( ProjectLitePermission, WorkspaceUserPermission, ) - from plane.db.models import Project, ProjectMember, IssueUserProperty, WorkspaceMember +from plane.ee.models import TeamSpaceMember, TeamSpaceProject from plane.bgtasks.project_add_user_email_task import project_add_user_email from plane.utils.host import base_host from plane.app.permissions.base import allow_permission, ROLE from plane.ee.bgtasks.project_activites_task import project_activity + class ProjectMemberViewSet(BaseViewSet): serializer_class = ProjectMemberAdminSerializer model = ProjectMember @@ -55,6 +57,35 @@ class ProjectMemberViewSet(BaseViewSet): .select_related("workspace", "workspace__owner") ) + def get_team_error_message( + self, + is_singular, + team_count, + type, + project_member_id=None, + requested_project_member_id=None, + ): + if type == "update_role": + return ( + "This project is linked to a team they are in. Remove them from that team first." + if is_singular + else f"This project is linked to {team_count} teams they are in. Remove them from those teams first." + ) + + return ( + ( + "This project is linked to a team you are in. Remove yourself from that team first." + if is_singular + else f"This project is linked to {team_count} teams you are in. Remove yourself from those teams first." + ) + if str(project_member_id) == str(requested_project_member_id) + else ( + "This project is linked to a team they are in. Remove them from that team first." + if is_singular + else f"This project is linked to {team_count} teams they are in. Remove them from those teams first." + ) + ) + @allow_permission([ROLE.ADMIN]) def create(self, request, slug, project_id): # Get the list of members to be added to the project and their roles i.e. the user_id and the role @@ -245,6 +276,28 @@ class ProjectMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + team_ids = TeamSpaceMember.objects.filter( + workspace__slug=slug, member_id=project_member.member_id + ).values_list("team_space_id", flat=True) + team_project_ids = TeamSpaceProject.objects.filter( + project_id=project_id, team_space_id__in=team_ids + ).values_list("team_space_id", flat=True) + if ( + "role" in request.data + and team_project_ids.exists() + and int(request.data.get("role")) < 10 + ): + team_count = team_project_ids.count() + is_singular = team_count == 1 + return Response( + { + "error": self.get_team_error_message( + is_singular, team_count, "update_role" + ) + }, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer = ProjectMemberSerializer( project_member, data=request.data, partial=True ) @@ -285,6 +338,32 @@ class ProjectMemberViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + # Check if this user is part of any team which is part of this project + team_ids = TeamSpaceMember.objects.filter( + workspace__slug=slug, member_id=project_member.member_id + ).values_list("team_space_id", flat=True) + + team_project_ids = TeamSpaceProject.objects.filter( + project_id=project_id, team_space_id__in=team_ids + ).values_list("team_space_id", flat=True) + + # Check if this project is part of any team + if team_project_ids.exists(): + team_count = team_project_ids.count() + is_singular = team_count == 1 + return Response( + { + "error": self.get_team_error_message( + is_singular, + team_count, + "leave", + project_member.id, + requesting_project_member.id, + ) + }, + status=status.HTTP_400_BAD_REQUEST, + ) + project_member.is_active = False project_member.save() project_activity.delay( @@ -293,11 +372,8 @@ class ProjectMemberViewSet(BaseViewSet): actor_id=str(request.user.id), project_id=str(project_id), current_instance=json.dumps( - { - "members": [str(project_member.member_id)], - "removed": True - } - ), + {"members": [str(project_member.member_id)], "removed": True} + ), epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), @@ -323,10 +399,40 @@ class ProjectMemberViewSet(BaseViewSet): ): return Response( { - "error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin" + "error": """ + You cannot leave the project as your the only admin of the project + you will have to either delete the project or create an another admin + """ }, status=status.HTTP_400_BAD_REQUEST, ) + + # Check if this user is part of any team which is part of this project + team_ids = TeamSpaceMember.objects.filter( + workspace__slug=slug, member_id=request.user.id + ).values_list("team_space_id", flat=True) + + team_project_ids = TeamSpaceProject.objects.filter( + project_id=project_id, team_space_id__in=team_ids + ).values_list("team_space_id", flat=True) + + # Check if this project is part of any team + if team_project_ids.exists(): + team_count = team_project_ids.count() + is_singular = team_count == 1 + return Response( + { + "error": self.get_team_error_message( + is_singular, + team_count, + "leave", + project_member.id, + project_member.id, + ) + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Deactivate the user project_member.is_active = False project_member.save() @@ -336,11 +442,8 @@ class ProjectMemberViewSet(BaseViewSet): actor_id=str(request.user.id), project_id=str(project_id), current_instance=json.dumps( - { - "members": [str(request.user.id)], - "removed": False - } - ), + {"members": [str(request.user.id)], "removed": False} + ), epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), diff --git a/apiserver/plane/ee/views/app/team/analytic.py b/apiserver/plane/ee/views/app/team/analytic.py index dcd63cc7d9..22a4c29c78 100644 --- a/apiserver/plane/ee/views/app/team/analytic.py +++ b/apiserver/plane/ee/views/app/team/analytic.py @@ -14,7 +14,12 @@ from rest_framework.response import Response # Module imports from plane.db.models import Cycle, Issue, IssueView, Page, IssueRelation, IssueAssignee -from plane.ee.models import TeamSpacePage, TeamSpaceProject, TeamSpaceView +from plane.ee.models import ( + TeamSpacePage, + TeamSpaceProject, + TeamSpaceView, + TeamSpaceMember, +) from plane.ee.permissions import TeamSpacePermission, WorkspaceUserPermission from plane.payment.flags.flag import FeatureFlag from plane.payment.flags.flag_decorator import check_feature_flag @@ -341,7 +346,12 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint): ) return Response(issue_map, status=status.HTTP_200_OK) - def member_tree(self, project_ids, filters): + def member_tree(self, team_space_id, project_ids, filters): + # Get all team member ids + team_member_ids = TeamSpaceMember.objects.filter( + team_space_id=team_space_id + ).values_list("member_id", flat=True) + issue_ids = ( Issue.issue_objects.filter(project_id__in=project_ids) .filter(**filters) @@ -349,7 +359,9 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint): ) # Get the issue map: issue_map = ( - IssueAssignee.objects.filter(issue__in=issue_ids) + IssueAssignee.objects.filter( + issue__in=issue_ids, assignee_id__in=team_member_ids + ) .annotate(identifier=F("assignee_id")) .values("identifier") .annotate(count=Count("id")) @@ -372,4 +384,4 @@ class TeamSpaceStatisticsEndpoint(TeamBaseEndpoint): if data_key == "projects": return self.project_tree(project_ids, filters) else: - return self.member_tree(project_ids, filters) + return self.member_tree(team_space_id, project_ids, filters) diff --git a/apiserver/plane/ee/views/app/team/member.py b/apiserver/plane/ee/views/app/team/member.py index cadad77fae..4ac31f2e09 100644 --- a/apiserver/plane/ee/views/app/team/member.py +++ b/apiserver/plane/ee/views/app/team/member.py @@ -105,13 +105,14 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint): team_space = TeamSpace.objects.get(workspace__slug=slug, pk=team_space_id) # Get the current team space members - current_team_space_members = list( - TeamSpaceMember.objects.filter( + current_members = list( + str(member_id) for member_id in TeamSpaceMember.objects.filter( workspace__slug=slug, team_space_id=team_space_id ).values_list("member_id", flat=True) ) + current_instance = json.dumps( - {"member_ids": current_team_space_members}, cls=DjangoJSONEncoder + {"member_ids": current_members}, cls=DjangoJSONEncoder ) # Get the list of team space members @@ -123,9 +124,20 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint): .order_by("sort_order") ) + # Set of newly added members + added_members = set(member_ids) - set(current_members) + + # Set of dropped members + dropped_members = set(current_members) - set(member_ids) + + # Remove dropped members from team + TeamSpaceMember.objects.filter( + workspace__slug=slug, team_space_id=team_space_id, member_id__in=dropped_members + ).delete() + # Create team space members bulk_team_space_members = [] - for member in member_ids: + for member in added_members: sort_order = next( ( member.get("sort_order") @@ -157,11 +169,6 @@ class TeamSpaceMembersEndpoint(TeamBaseEndpoint): workspace__slug=slug, team_space_id=team_space_id ) - # add current members also to requested data to make it consistent - request.data["member_ids"] = [ - str(mem) for mem in current_team_space_members - ] + request.data.get("member_ids", []) - # Create activity team_space_activity.delay( type="team_space.activity.updated", diff --git a/web/ee/components/teams/actions/members/modal.tsx b/web/ee/components/teams/actions/members/modal.tsx index 547a4d8be9..ed5780a544 100644 --- a/web/ee/components/teams/actions/members/modal.tsx +++ b/web/ee/components/teams/actions/members/modal.tsx @@ -1,4 +1,5 @@ import { FC, useEffect, useMemo, useState } from "react"; +import uniq from "lodash/uniq"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { ChevronDown, Plus, X } from "lucide-react"; @@ -17,7 +18,8 @@ import { import { getFileURL } from "@/helpers/file.helper"; // hooks import { useMember } from "@/hooks/store"; -// plane web hooks +// plane web imports +import { EUserPermissions } from "@/plane-web/constants"; import { useTeams } from "@/plane-web/hooks/store"; type Props = { @@ -32,51 +34,53 @@ export const AddTeamMembersModal: FC = observer((props) => { const { workspaceSlug } = useParams(); // states const [isSubmitting, setIsSubmitting] = useState(false); - const [teamMemberIds, setTeamMemberIds] = useState([]); + const [memberIdsToInvite, setMemberIdsToInvite] = useState([]); // store hooks const { workspace: { workspaceMemberIds, getWorkspaceMemberDetails }, } = useMember(); - const { getTeamById, getTeamMemberIds, addTeamMembers } = useTeams(); + const { getTeamById, getTeamMemberIds, updateTeamMembers } = useTeams(); // derived values const teamDetail = teamId ? getTeamById(teamId) : undefined; - const memberIds = teamId ? getTeamMemberIds(teamId) : []; + const teamMemberIds = teamId ? getTeamMemberIds(teamId) : []; const uninvitedPeople = useMemo( () => workspaceMemberIds?.filter((userId) => { - const isInvited = memberIds?.find((u) => u === userId) || teamMemberIds?.find((u) => u === userId); + const memberDetails = getWorkspaceMemberDetails(userId); + if (memberDetails?.role === EUserPermissions.GUEST) return false; + const isInvited = teamMemberIds?.find((u) => u === userId) || memberIdsToInvite?.find((u) => u === userId); return !isInvited; }), // eslint-disable-next-line react-hooks/exhaustive-deps - [teamMemberIds] + [memberIdsToInvite] ); const isButtonDisabled = useMemo( - () => teamMemberIds.length === 0 || teamMemberIds.some((id) => !id), - [teamMemberIds] + () => memberIdsToInvite.length === 0 || memberIdsToInvite.some((id) => !id), + [memberIdsToInvite] ); useEffect(() => { if (isModalOpen) { if (teamDetail) { - setTeamMemberIds([""]); + setMemberIdsToInvite([""]); } else { - setTeamMemberIds([""]); + setMemberIdsToInvite([""]); } } }, [teamDetail, isModalOpen]); const handleModalClearAndClose = () => { - setTeamMemberIds([]); + setMemberIdsToInvite([]); handleModalClose(); }; const handleAddTeamMembers = async () => { - if (!teamId || !teamMemberIds) return; + if (!teamId || !memberIdsToInvite) return; // clean up team member ids - setTeamMemberIds(teamMemberIds.filter((id) => id !== "")); + setMemberIdsToInvite(memberIdsToInvite.filter((id) => id !== "")); setIsSubmitting(true); - await addTeamMembers(workspaceSlug?.toString(), teamId, teamMemberIds) + await updateTeamMembers(workspaceSlug?.toString(), teamId, uniq([...(teamMemberIds ?? []), ...memberIdsToInvite])) .then(() => { handleModalClearAndClose(); setToast({ @@ -140,7 +144,7 @@ export const AddTeamMembersModal: FC = observer((props) => {

Add coworkers, clients, and consultants

- {teamMemberIds.map((memberId, index) => { + {memberIdsToInvite.map((memberId, index) => { const selectedMember = getWorkspaceMemberDetails(memberId); return (
@@ -164,11 +168,11 @@ export const AddTeamMembersModal: FC = observer((props) => { } onChange={(val: string) => { - setTeamMemberIds( - teamMemberIds.map((_, i) => { + setMemberIdsToInvite( + memberIdsToInvite.map((_, i) => { if (i === index) { // Don't add if value already exists in array - if (teamMemberIds.includes(val)) return ""; + if (memberIdsToInvite.includes(val)) return ""; return val; } return _; @@ -180,12 +184,12 @@ export const AddTeamMembersModal: FC = observer((props) => { />
- {teamMemberIds.length > 1 && ( + {memberIdsToInvite.length > 1 && (
@@ -201,7 +205,7 @@ export const AddTeamMembersModal: FC = observer((props) => {
diff --git a/web/ee/components/teams/create-update/form.tsx b/web/ee/components/teams/create-update/form.tsx index 99e86b9818..33e490b50d 100644 --- a/web/ee/components/teams/create-update/form.tsx +++ b/web/ee/components/teams/create-update/form.tsx @@ -1,14 +1,13 @@ "use client"; -import { FormEvent, useState } from "react"; +import { FormEvent, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { InfoIcon } from "lucide-react"; // types import { TTeam } from "@plane/types"; import { EFileAssetType } from "@plane/types/src/enums"; // ui -import { Button, CustomEmojiIconPicker, Input, Logo, Tooltip } from "@plane/ui"; +import { Button, CustomEmojiIconPicker, Input, Logo } from "@plane/ui"; // components import { MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; import { RichTextEditor } from "@/components/editor"; @@ -17,8 +16,9 @@ import { cn } from "@/helpers/common.helper"; import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getDescriptionPlaceholder } from "@/helpers/issue.helper"; // store hooks -import { useWorkspace } from "@/hooks/store"; +import { useMember, useWorkspace } from "@/hooks/store"; // plane web components +import { EUserPermissions } from "@/plane-web/constants"; import { useTeams } from "@/plane-web/hooks/store"; import { useEditorMentionSearch } from "@/plane-web/hooks/use-editor-mention-search"; // services @@ -44,9 +44,20 @@ export const CreateOrUpdateTeamForm: React.FC = observer((props) => { const [errors, setErrors] = useState<{ name?: string }>({}); // store hooks const { currentWorkspace } = useWorkspace(); + const { + workspace: { workspaceMemberIds, getWorkspaceMemberDetails }, + } = useMember(); const { getTeamMemberIds } = useTeams(); // derived values const teamMemberIds = teamDetail?.id ? (getTeamMemberIds(teamDetail.id) ?? []) : (formData?.member_ids ?? []); + const userIdsWithAdminOrMemberRole = useMemo( + () => + workspaceMemberIds?.filter((userId) => { + const memberDetails = getWorkspaceMemberDetails(userId); + return memberDetails?.role === EUserPermissions.GUEST ? false : true; + }), + [workspaceMemberIds, getWorkspaceMemberDetails] + ); // use editor mention search const { searchEntity } = useEditorMentionSearch({ memberIds: teamMemberIds, @@ -161,6 +172,7 @@ export const CreateOrUpdateTeamForm: React.FC = observer((props) => {

Team lead

{ if (val && val !== formData.lead_id) { handleFormDataChange("lead_id", val); @@ -180,21 +192,12 @@ export const CreateOrUpdateTeamForm: React.FC = observer((props) => { />
-

- {teamDetail?.id ? "Add team members" : "Team members"} - {teamDetail?.id && ( - - - - )} -

+

Team members

{ - handleFormDataChange("member_ids", Array.from(new Set([...(teamDetail?.member_ids ?? []), ...val]))); + handleFormDataChange("member_ids", val); }} multiple buttonVariant="border-with-text" diff --git a/web/ee/components/teams/overview/sidebar/properties/root.tsx b/web/ee/components/teams/overview/sidebar/properties/root.tsx index 9807053c4a..7f30ffd5d4 100644 --- a/web/ee/components/teams/overview/sidebar/properties/root.tsx +++ b/web/ee/components/teams/overview/sidebar/properties/root.tsx @@ -11,6 +11,8 @@ import { Props } from "@/components/icons/types"; // hooks import { cn } from "@/helpers/common.helper"; // plane web hooks +import { useMember } from "@/hooks/store"; +import { EUserPermissions } from "@/plane-web/constants"; import { useTeams } from "@/plane-web/hooks/store"; // local components import { TeamsPropertiesList } from "./list"; @@ -34,6 +36,9 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid // router const { workspaceSlug } = useParams(); // hooks + const { + workspace: { workspaceMemberIds, getWorkspaceMemberDetails }, + } = useMember(); const { getTeamById, getTeamEntitiesById, getTeamEntitiesLoaderById, updateTeam } = useTeams(); // derived values const team = getTeamById(teamId); @@ -41,6 +46,14 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid const teamEntitiesLoader = getTeamEntitiesLoaderById(teamId); const linkedEntitiesCount = teamEntities?.linked_entities.total; const teamEntitiesCount = teamEntities?.team_entities.total; + const userIdsWithAdminOrMemberRole = useMemo( + () => + workspaceMemberIds?.filter((userId) => { + const memberDetails = getWorkspaceMemberDetails(userId); + return memberDetails?.role === EUserPermissions.GUEST ? false : true; + }), + [workspaceMemberIds, getWorkspaceMemberDetails] + ); if (!team) return null; @@ -64,6 +77,7 @@ export const TeamsOverviewSidebarProperties = observer((props: TTeamsOverviewSid value: ( { - const { title, icon, value, containerClassName, titleClassName, valueClassName } = props; - - return ( -
- - {icon} - {title} - - {value} -
- ); -}; - -type TTeamWorkloadDetailProps = { - teamId: string; -}; - -export const TeamWorkloadDetail: React.FC = observer((props) => { - const { teamId } = props; - - return ( -
-

Summary of team's issues

-
-
- - 12 issues are not assigned due date -
-
- - } - value="12" - titleClassName={TITLE_CLASSNAME} - containerClassName={ITEM_CONTAINER_CLASSNAME} - valueClassName={cn(VALUE_CLASSNAME, "bg-red-500 text-white px-0.5 rounded")} - /> - } - value={ - <> -
- 120 - - } - titleClassName={TITLE_CLASSNAME} - containerClassName={ITEM_CONTAINER_CLASSNAME} - valueClassName={cn(VALUE_CLASSNAME, "relative bg-custom-background-80/80 px-0.5 rounded")} - /> - } - value="140" - titleClassName={TITLE_CLASSNAME} - containerClassName={ITEM_CONTAINER_CLASSNAME} - valueClassName={cn(VALUE_CLASSNAME)} - /> -
-
- - - -
-
-
- ); -}); diff --git a/web/ee/components/teams/overview/workload/summary.tsx b/web/ee/components/teams/overview/workload/summary.tsx index 01953d4ce1..59ec4ad120 100644 --- a/web/ee/components/teams/overview/workload/summary.tsx +++ b/web/ee/components/teams/overview/workload/summary.tsx @@ -61,7 +61,7 @@ export const TeamWorkloadSummary: React.FC = observer {isUpdating && }
- {teamWorkloadSummary && teamWorkloadSummary.no_due_date_issues > 0 ? ( + {teamWorkloadSummary ? (
{teamWorkloadSummary.no_due_date_issues} issues are not assigned due date diff --git a/web/ee/components/teams/views/view-list-item-action.tsx b/web/ee/components/teams/views/view-list-item-action.tsx index f25f2811ce..8efdec4f9b 100644 --- a/web/ee/components/teams/views/view-list-item-action.tsx +++ b/web/ee/components/teams/views/view-list-item-action.tsx @@ -58,13 +58,13 @@ export const TeamViewListItemAction: FC = observer((props) => { // handlers const handleAddToFavorites = () => { - if (!workspaceSlug || !teamId) return; + if (!workspaceSlug || !teamId || view.is_team_view) return; addViewToFavorites(workspaceSlug.toString(), teamId.toString(), view.id); }; const handleRemoveFromFavorites = () => { - if (!workspaceSlug || !teamId) return; + if (!workspaceSlug || !teamId || view.is_team_view) return; removeViewFromFavorites(workspaceSlug.toString(), teamId.toString(), view.id); }; @@ -96,7 +96,7 @@ export const TeamViewListItemAction: FC = observer((props) => { {/* created by */} {} - {isEditingAllowed && ( + {isEditingAllowed && !view.is_team_view && ( { e.preventDefault(); diff --git a/web/ee/store/team/pages/team-page.ts b/web/ee/store/team/pages/team-page.ts index c4d6a70cb5..1deb30a7b6 100644 --- a/web/ee/store/team/pages/team-page.ts +++ b/web/ee/store/team/pages/team-page.ts @@ -143,8 +143,9 @@ export class TeamPage extends BasePage implements TTeamPage { * @description returns true if the current logged in user can favorite the page */ get canCurrentUserFavoritePage() { - const userRole = this.getUserWorkspaceRole(); - return !!userRole && userRole >= EUserPermissions.MEMBER; + return false; + // const userRole = this.getUserWorkspaceRole(); + // return !!userRole && userRole >= EUserPermissions.MEMBER; } /** diff --git a/web/ee/store/team/team.store.ts b/web/ee/store/team/team.store.ts index e0841cc4d8..3dc0c56d5c 100644 --- a/web/ee/store/team/team.store.ts +++ b/web/ee/store/team/team.store.ts @@ -1,4 +1,5 @@ import set from "lodash/set"; +import uniq from "lodash/uniq"; import update from "lodash/update"; import xor from "lodash/xor"; import { makeObservable, action, computed, observable, runInAction } from "mobx"; @@ -50,7 +51,7 @@ export interface ITeamStore { // CURD actions createTeam: (workspaceSlug: string, data: Partial) => Promise; updateTeam: (workspaceSlug: string, teamId: string, data: Partial) => Promise; - addTeamMembers: ( + updateTeamMembers: ( workspaceSlug: string, teamId: string, memberIds: string[], @@ -100,7 +101,7 @@ export class TeamStore implements ITeamStore { // CURD actions createTeam: action, updateTeam: action, - addTeamMembers: action, + updateTeamMembers: action, removeTeamMember: action, joinTeam: action, deleteTeam: action, @@ -378,8 +379,8 @@ export class TeamStore implements ITeamStore { // create team const team = await this.teamService.createTeam(workspaceSlug, data); // add team members - if (data.member_ids) { - await this.addTeamMembers(workspaceSlug, team.id, data.member_ids, false); + if (data.member_ids && data.member_ids.length > 0) { + await this.updateTeamMembers(workspaceSlug, team.id, data.member_ids, false); } // set team map along with member_ids runInAction(() => { @@ -407,21 +408,20 @@ export class TeamStore implements ITeamStore { const teamDetailsBeforeUpdate = this.getTeamById(teamId); // remove member_ids from data const { member_ids, ...payload } = data; - // get member ids to be added - const memberIdsToBeAdded = member_ids - ? member_ids.filter((memberId) => !teamDetailsBeforeUpdate.member_ids?.includes(memberId)) - : []; // Check if team projects have been updated const areProjectsUpdated = !!data.project_ids && xor(teamDetailsBeforeUpdate.project_ids, data.project_ids).length > 0; + + // Check if team members have been updated + const areMembersUpdated = !!member_ids && xor(teamDetailsBeforeUpdate.member_ids, member_ids).length > 0; try { // update team map runInAction(() => { update(this.teamMap, [teamId], (team) => ({ ...team, ...data })); }); // add team members if any - if (memberIdsToBeAdded.length > 0) { - await this.addTeamMembers(workspaceSlug, teamId, memberIdsToBeAdded, false); + if (areMembersUpdated) { + await this.updateTeamMembers(workspaceSlug, teamId, member_ids, false); } // update team const team = await this.teamService.updateTeam(workspaceSlug, teamId, payload); @@ -445,14 +445,14 @@ export class TeamStore implements ITeamStore { }; /** - * Adds team members to a team and updates the store + * Updates team members of a team and updates the store * @param workspaceSlug * @param teamId * @param memberIds * @param updateTeamDetails * @returns Promise */ - addTeamMembers = async ( + updateTeamMembers = async ( workspaceSlug: string, teamId: string, memberIds: string[], @@ -461,8 +461,10 @@ export class TeamStore implements ITeamStore { try { // add team members const teamMembers = await this.teamService.addTeamMembers(workspaceSlug, teamId, memberIds); - // set team members map runInAction(() => { + // clear team members map + set(this.teamMembersMap, [teamId], {}); + // set team members map teamMembers.forEach((member) => { set(this.teamMembersMap, [teamId, member.member], member); }); @@ -529,8 +531,10 @@ export class TeamStore implements ITeamStore { return; } this.loader = "mutation"; + // get team member ids + const teamMemberIds = this.getTeamMemberIds(teamId); // join team - await this.addTeamMembers(workspaceSlug, teamId, [currentUserId]); + await this.updateTeamMembers(workspaceSlug, teamId, uniq([...(teamMemberIds ?? []), currentUserId]), false); this.loader = "loaded"; } catch (error) { console.error("Failed to join team", error);