import set from "lodash/set"; import sortBy from "lodash/sortBy"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // services import { EUserWorkspaceRoles } from "constants/workspace"; import { WorkspaceService } from "services/workspace.service"; // types import { IRouterStore } from "store/application/router.store"; import { RootStore } from "store/root.store"; import { IUserRootStore } from "store/user"; import { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types"; // constants import { IMemberRootStore } from "."; export interface IWorkspaceMembership { id: string; member: string; role: EUserWorkspaceRoles; } export interface IWorkspaceMemberStore { // observables workspaceMemberMap: Record>; workspaceMemberInvitations: Record; // computed workspaceMemberIds: string[] | null; workspaceMemberInvitationIds: string[] | null; memberMap: Record | null; // computed actions getSearchedWorkspaceMemberIds: (searchQuery: string) => string[] | null; getSearchedWorkspaceInvitationIds: (searchQuery: string) => string[] | null; getWorkspaceMemberDetails: (workspaceMemberId: string) => IWorkspaceMember | null; getWorkspaceInvitationDetails: (invitationId: string) => IWorkspaceMemberInvitation | null; // fetch actions fetchWorkspaceMembers: (workspaceSlug: string) => Promise; fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise; // crud actions updateMember: (workspaceSlug: string, userId: string, data: { role: EUserWorkspaceRoles }) => Promise; removeMemberFromWorkspace: (workspaceSlug: string, userId: string) => Promise; // invite actions inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise; updateMemberInvitation: ( workspaceSlug: string, invitationId: string, data: Partial ) => Promise; deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise; } export class WorkspaceMemberStore implements IWorkspaceMemberStore { // observables workspaceMemberMap: { [workspaceSlug: string]: Record; } = {}; // { workspaceSlug: { userId: userDetails } } workspaceMemberInvitations: Record = {}; // { workspaceSlug: [invitations] } // stores routerStore: IRouterStore; userStore: IUserRootStore; memberRoot: IMemberRootStore; // services workspaceService; constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) { makeObservable(this, { // observables workspaceMemberMap: observable, workspaceMemberInvitations: observable, // computed workspaceMemberIds: computed, workspaceMemberInvitationIds: computed, memberMap: computed, // actions fetchWorkspaceMembers: action, updateMember: action, removeMemberFromWorkspace: action, fetchWorkspaceMemberInvitations: action, updateMemberInvitation: action, deleteMemberInvitation: action, }); // root store this.routerStore = _rootStore.app.router; this.userStore = _rootStore.user; this.memberRoot = _memberRoot; // services this.workspaceService = new WorkspaceService(); } /** * @description get the list of all the user ids of all the members of the current workspace */ get workspaceMemberIds() { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {}); members = sortBy(members, [ (m) => m.member !== this.userStore.currentUser?.id, (m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(), ]); //filter out bots const memberIds = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot).map((m) => m.member); return memberIds; } get memberMap() { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; return this.workspaceMemberMap?.[workspaceSlug] ?? {}; } get workspaceMemberInvitationIds() { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; return this.workspaceMemberInvitations?.[workspaceSlug]?.map((inv) => inv.id); } /** * @description get the list of all the user ids that match the search query of all the members of the current workspace * @param searchQuery */ getSearchedWorkspaceMemberIds = computedFn((searchQuery: string) => { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; const workspaceMemberIds = this.workspaceMemberIds; if (!workspaceMemberIds) return null; const searchedWorkspaceMemberIds = workspaceMemberIds?.filter((userId) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) return false; const memberSearchQuery = `${memberDetails.member.first_name} ${memberDetails.member.last_name} ${ memberDetails.member.display_name } ${memberDetails.member.email ?? ""}`; return memberSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); }); return searchedWorkspaceMemberIds; }); /** * @description get the list of all the invitation ids that match the search query of all the member invitations of the current workspace * @param searchQuery */ getSearchedWorkspaceInvitationIds = computedFn((searchQuery: string) => { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; const workspaceMemberInvitationIds = this.workspaceMemberInvitationIds; if (!workspaceMemberInvitationIds) return null; const searchedWorkspaceMemberInvitationIds = workspaceMemberInvitationIds.filter((invitationId) => { const invitationDetails = this.getWorkspaceInvitationDetails(invitationId); if (!invitationDetails) return false; const invitationSearchQuery = `${invitationDetails.email}`; return invitationSearchQuery.toLowerCase()?.includes(searchQuery.toLowerCase()); }); return searchedWorkspaceMemberInvitationIds; }); /** * @description get the details of a workspace member * @param userId */ getWorkspaceMemberDetails = computedFn((userId: string) => { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId]; if (!workspaceMember) return null; const memberDetails: IWorkspaceMember = { id: workspaceMember.id, role: workspaceMember.role, member: this.memberRoot?.memberMap?.[workspaceMember.member], }; return memberDetails; }); /** * @description get the details of a workspace member invitation * @param workspaceSlug * @param memberId */ getWorkspaceInvitationDetails = computedFn((invitationId: string) => { const workspaceSlug = this.routerStore.workspaceSlug; if (!workspaceSlug) return null; const invitationsList = this.workspaceMemberInvitations?.[workspaceSlug]; if (!invitationsList) return null; const invitation = invitationsList.find((inv) => inv.id === invitationId); return invitation ?? null; }); /** * @description fetch all the members of a workspace * @param workspaceSlug */ fetchWorkspaceMembers = async (workspaceSlug: string) => await this.workspaceService.fetchWorkspaceMembers(workspaceSlug).then((response) => { runInAction(() => { response.forEach((member) => { set(this.memberRoot?.memberMap, member.member.id, member.member); set(this.workspaceMemberMap, [workspaceSlug, member.member.id], { id: member.id, member: member.member.id, role: member.role, }); }); }); return response; }); /** * @description update the role of a workspace member * @param workspaceSlug * @param userId * @param data */ updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserWorkspaceRoles }) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); // original data to revert back in case of error const originalProjectMemberData = this.workspaceMemberMap?.[workspaceSlug]?.[userId]; try { runInAction(() => { set(this.workspaceMemberMap, [workspaceSlug, userId, "role"], data.role); }); await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberDetails.id, data); } catch (error) { // revert back to original members in case of error runInAction(() => { set(this.workspaceMemberMap, [workspaceSlug, userId], originalProjectMemberData); }); throw error; } }; /** * @description remove a member from workspace * @param workspaceSlug * @param userId */ removeMemberFromWorkspace = async (workspaceSlug: string, userId: string) => { const memberDetails = this.getWorkspaceMemberDetails(userId); if (!memberDetails) throw new Error("Member not found"); await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberDetails?.id).then(() => { runInAction(() => { delete this.memberRoot?.memberMap?.[userId]; delete this.workspaceMemberMap?.[workspaceSlug]?.[userId]; }); }); }; /** * @description fetch all the member invitations of a workspace * @param workspaceSlug */ fetchWorkspaceMemberInvitations = async (workspaceSlug: string) => await this.workspaceService.workspaceInvitations(workspaceSlug).then((response) => { runInAction(() => { set(this.workspaceMemberInvitations, workspaceSlug, response); }); return response; }); /** * @description bulk invite members to a workspace * @param workspaceSlug * @param data */ inviteMembersToWorkspace = async (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => { const response = await this.workspaceService.inviteWorkspace(workspaceSlug, data); await this.fetchWorkspaceMemberInvitations(workspaceSlug); return response; }; /** * @description update the role of a member invitation * @param workspaceSlug * @param invitationId * @param data */ updateMemberInvitation = async ( workspaceSlug: string, invitationId: string, data: Partial ) => { const originalMemberInvitations = [...this.workspaceMemberInvitations?.[workspaceSlug]]; // in case of error, we will revert back to original members try { const memberInvitations = originalMemberInvitations?.map((invitation) => ({ ...invitation, ...(invitation.id === invitationId && data), })); // optimistic update runInAction(() => { set(this.workspaceMemberInvitations, workspaceSlug, memberInvitations); }); await this.workspaceService.updateWorkspaceInvitation(workspaceSlug, invitationId, data); } catch (error) { // revert back to original members in case of error runInAction(() => { set(this.workspaceMemberInvitations, workspaceSlug, originalMemberInvitations); }); throw error; } }; /** * @description delete a member invitation * @param workspaceSlug * @param memberId */ deleteMemberInvitation = async (workspaceSlug: string, invitationId: string) => await this.workspaceService.deleteWorkspaceInvitations(workspaceSlug.toString(), invitationId).then(() => { runInAction(() => { this.workspaceMemberInvitations[workspaceSlug] = this.workspaceMemberInvitations[workspaceSlug].filter( (inv) => inv.id !== invitationId ); }); }); }