feat: init
This commit is contained in:
51
apps/web/core/store/member/index.ts
Normal file
51
apps/web/core/store/member/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import type { IUserLite } from "@plane/types";
|
||||
// plane web imports
|
||||
import type { IProjectMemberStore } from "@/plane-web/store/member/project-member.store";
|
||||
import { ProjectMemberStore } from "@/plane-web/store/member/project-member.store";
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
// local imports
|
||||
import type { IWorkspaceMemberStore } from "./workspace/workspace-member.store";
|
||||
import { WorkspaceMemberStore } from "./workspace/workspace-member.store";
|
||||
|
||||
export interface IMemberRootStore {
|
||||
// observables
|
||||
memberMap: Record<string, IUserLite>;
|
||||
// computed actions
|
||||
getMemberIds: () => string[];
|
||||
getUserDetails: (userId: string) => IUserLite | undefined;
|
||||
// sub-stores
|
||||
workspace: IWorkspaceMemberStore;
|
||||
project: IProjectMemberStore;
|
||||
}
|
||||
|
||||
export class MemberRootStore implements IMemberRootStore {
|
||||
// observables
|
||||
memberMap: Record<string, IUserLite> = {};
|
||||
// sub-stores
|
||||
workspace: IWorkspaceMemberStore;
|
||||
project: IProjectMemberStore;
|
||||
|
||||
constructor(_rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
memberMap: observable,
|
||||
});
|
||||
// sub-stores
|
||||
this.workspace = new WorkspaceMemberStore(this, _rootStore);
|
||||
this.project = new ProjectMemberStore(this, _rootStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get all member ids
|
||||
*/
|
||||
getMemberIds = computedFn(() => Object.keys(this.memberMap));
|
||||
|
||||
/**
|
||||
* @description get user details from userId
|
||||
* @param userId
|
||||
*/
|
||||
getUserDetails = computedFn((userId: string): IUserLite | undefined => this.memberMap?.[userId] ?? undefined);
|
||||
}
|
||||
410
apps/web/core/store/member/project/base-project-member.store.ts
Normal file
410
apps/web/core/store/member/project/base-project-member.store.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { uniq, unset, set, update, sortBy } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
import type { EUserProjectRoles, IProjectBulkAddFormData, IUserLite, TProjectMembership } from "@plane/types";
|
||||
// plane web imports
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
// services
|
||||
import { ProjectMemberService } from "@/services/project";
|
||||
// store
|
||||
import type { IProjectStore } from "@/store/project/project.store";
|
||||
import type { IRouterStore } from "@/store/router.store";
|
||||
import type { IUserStore } from "@/store/user";
|
||||
// local imports
|
||||
import type { IMemberRootStore } from "../index";
|
||||
import { sortProjectMembers } from "../utils";
|
||||
import type { IProjectMemberFiltersStore } from "./project-member-filters.store";
|
||||
import { ProjectMemberFiltersStore } from "./project-member-filters.store";
|
||||
|
||||
export interface IProjectMemberDetails extends Omit<TProjectMembership, "member"> {
|
||||
member: IUserLite;
|
||||
}
|
||||
|
||||
export interface IBaseProjectMemberStore {
|
||||
// observables
|
||||
projectMemberFetchStatusMap: {
|
||||
[projectId: string]: boolean;
|
||||
};
|
||||
projectMemberMap: {
|
||||
[projectId: string]: Record<string, TProjectMembership>;
|
||||
};
|
||||
// filters store
|
||||
filters: IProjectMemberFiltersStore;
|
||||
// computed
|
||||
projectMemberIds: string[] | null;
|
||||
// computed actions
|
||||
getProjectMemberFetchStatus: (projectId: string) => boolean;
|
||||
getProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||
getProjectMemberIds: (projectId: string, includeGuestUsers: boolean) => string[] | null;
|
||||
getFilteredProjectMemberDetails: (userId: string, projectId: string) => IProjectMemberDetails | null;
|
||||
// fetch actions
|
||||
fetchProjectMembers: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
clearExistingMembers?: boolean
|
||||
) => Promise<TProjectMembership[]>;
|
||||
// bulk operation actions
|
||||
bulkAddMembersToProject: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
data: IProjectBulkAddFormData
|
||||
) => Promise<TProjectMembership[]>;
|
||||
// crud actions
|
||||
updateMemberRole: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
userId: string,
|
||||
role: EUserProjectRoles
|
||||
) => Promise<TProjectMembership>;
|
||||
removeMemberFromProject: (workspaceSlug: string, projectId: string, userId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore {
|
||||
// observables
|
||||
projectMemberFetchStatusMap: {
|
||||
[projectId: string]: boolean;
|
||||
} = {};
|
||||
projectMemberMap: {
|
||||
[projectId: string]: Record<string, TProjectMembership>;
|
||||
} = {};
|
||||
// filters store
|
||||
filters: IProjectMemberFiltersStore;
|
||||
// stores
|
||||
routerStore: IRouterStore;
|
||||
userStore: IUserStore;
|
||||
memberRoot: IMemberRootStore;
|
||||
projectRoot: IProjectStore;
|
||||
rootStore: RootStore;
|
||||
// services
|
||||
projectMemberService;
|
||||
|
||||
constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
projectMemberMap: observable,
|
||||
// computed
|
||||
projectMemberIds: computed,
|
||||
// actions
|
||||
fetchProjectMembers: action,
|
||||
bulkAddMembersToProject: action,
|
||||
updateMemberRole: action,
|
||||
removeMemberFromProject: action,
|
||||
});
|
||||
// root store
|
||||
this.rootStore = _rootStore;
|
||||
this.routerStore = _rootStore.router;
|
||||
this.userStore = _rootStore.user;
|
||||
this.memberRoot = _memberRoot;
|
||||
this.projectRoot = _rootStore.projectRoot.project;
|
||||
this.filters = new ProjectMemberFiltersStore();
|
||||
// services
|
||||
this.projectMemberService = new ProjectMemberService();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get the list of all the user ids of all the members of the current project
|
||||
* Returns filtered and sorted member IDs based on current filters
|
||||
*/
|
||||
get projectMemberIds() {
|
||||
const projectId = this.routerStore.projectId;
|
||||
if (!projectId) return null;
|
||||
|
||||
const members = Object.values(this.projectMemberMap?.[projectId] ?? {});
|
||||
if (members.length === 0) return null;
|
||||
|
||||
// Access the filters directly to ensure MobX tracking
|
||||
const currentFilters = this.filters.filtersMap[projectId];
|
||||
|
||||
// Apply filters and sorting directly here to ensure MobX tracking
|
||||
const sortedMembers = sortProjectMembers(
|
||||
members,
|
||||
this.memberRoot?.memberMap || {},
|
||||
(member) => member.member,
|
||||
currentFilters
|
||||
);
|
||||
|
||||
return sortedMembers.map((member) => member.member);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get the fetch status of a project member
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectMemberFetchStatus = computedFn((projectId: string) => this.projectMemberFetchStatusMap?.[projectId]);
|
||||
|
||||
/**
|
||||
* @description get the project memberships
|
||||
* @param projectId
|
||||
*/
|
||||
protected getProjectMemberships = computedFn((projectId: string) =>
|
||||
Object.values(this.projectMemberMap?.[projectId] ?? {})
|
||||
);
|
||||
|
||||
/**
|
||||
* @description get the project membership by user id
|
||||
* @param userId
|
||||
* @param projectId
|
||||
*/
|
||||
protected getProjectMembershipByUserId = computedFn(
|
||||
(userId: string, projectId: string) => this.projectMemberMap?.[projectId]?.[userId]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description get the role from the project membership
|
||||
* @param userId
|
||||
* @param projectId
|
||||
*/
|
||||
protected getRoleFromProjectMembership = computedFn(
|
||||
(userId: string, projectId: string): EUserProjectRoles | undefined => {
|
||||
const projectMembership = this.getProjectMembershipByUserId(userId, projectId);
|
||||
if (!projectMembership) return undefined;
|
||||
const projectMembershipRole = projectMembership.original_role ?? projectMembership.role;
|
||||
return projectMembershipRole ? (projectMembershipRole as EUserProjectRoles) : undefined;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Returns the project membership role for a user
|
||||
* @description This method is specifically used when adding new members to a project. For existing members,
|
||||
* the role is fetched directly from the backend during member listing.
|
||||
* @param { string } userId - The ID of the user
|
||||
* @param { string } projectId - The ID of the project
|
||||
* @returns { EUserProjectRoles | undefined } The user's role in the project, or undefined if not found
|
||||
*/
|
||||
abstract getUserProjectRole: (userId: string, projectId: string) => EUserProjectRoles | undefined;
|
||||
|
||||
/**
|
||||
* @description get the details of a project member
|
||||
* @param userId
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
||||
const projectMember = this.getProjectMembershipByUserId(userId, projectId);
|
||||
const userDetails = this.memberRoot?.memberMap?.[projectMember?.member];
|
||||
if (!projectMember || !userDetails) return null;
|
||||
const memberDetails: IProjectMemberDetails = {
|
||||
id: projectMember.id,
|
||||
role: projectMember.role,
|
||||
original_role: projectMember.original_role,
|
||||
member: {
|
||||
...userDetails,
|
||||
joining_date: projectMember.created_at ?? undefined,
|
||||
},
|
||||
created_at: projectMember.created_at,
|
||||
};
|
||||
return memberDetails;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the list of all the user ids of all the members of a project using projectId
|
||||
* @param projectId
|
||||
*/
|
||||
getProjectMemberIds = computedFn((projectId: string, includeGuestUsers: boolean): string[] | null => {
|
||||
if (!this.projectMemberMap?.[projectId]) return null;
|
||||
let members = this.getProjectMemberships(projectId);
|
||||
if (includeGuestUsers === false) {
|
||||
members = members.filter((m) => m.role !== EUserPermissions.GUEST);
|
||||
}
|
||||
members = sortBy(members, [
|
||||
(m) => m.member !== this.userStore.data?.id,
|
||||
(m) => this.memberRoot?.memberMap?.[m.member]?.display_name?.toLowerCase(),
|
||||
]);
|
||||
const memberIds = members.map((m) => m.member);
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the filtered project member details for a specific user
|
||||
* @param userId
|
||||
* @param projectId
|
||||
*/
|
||||
getFilteredProjectMemberDetails = computedFn((userId: string, projectId: string) => {
|
||||
const projectMember = this.getProjectMembershipByUserId(userId, projectId);
|
||||
const userDetails = this.memberRoot?.memberMap?.[projectMember?.member];
|
||||
if (!projectMember || !userDetails) return null;
|
||||
|
||||
// Check if this member passes the current filters
|
||||
const allMembers = this.getProjectMemberships(projectId);
|
||||
const filteredMemberIds = this.filters.getFilteredMemberIds(
|
||||
allMembers,
|
||||
this.memberRoot?.memberMap || {},
|
||||
(member) => member.member,
|
||||
projectId
|
||||
);
|
||||
|
||||
// Return null if this user doesn't pass the filters
|
||||
if (!filteredMemberIds.includes(userId)) return null;
|
||||
|
||||
const memberDetails: IProjectMemberDetails = {
|
||||
id: projectMember.id,
|
||||
role: projectMember.role,
|
||||
original_role: projectMember.original_role,
|
||||
member: {
|
||||
...userDetails,
|
||||
joining_date: projectMember.created_at ?? undefined,
|
||||
},
|
||||
created_at: projectMember.created_at,
|
||||
};
|
||||
return memberDetails;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description fetch the list of all the members of a project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchProjectMembers = async (workspaceSlug: string, projectId: string, clearExistingMembers: boolean = false) =>
|
||||
await this.projectMemberService.fetchProjectMembers(workspaceSlug, projectId).then((response) => {
|
||||
runInAction(() => {
|
||||
if (clearExistingMembers) {
|
||||
unset(this.projectMemberMap, [projectId]);
|
||||
}
|
||||
response.forEach((member) => {
|
||||
set(this.projectMemberMap, [projectId, member.member], member);
|
||||
});
|
||||
set(this.projectMemberFetchStatusMap, [projectId], true);
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description bulk add members to a project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param data
|
||||
* @returns Promise<TProjectMembership[]>
|
||||
*/
|
||||
bulkAddMembersToProject = async (workspaceSlug: string, projectId: string, data: IProjectBulkAddFormData) =>
|
||||
await this.projectMemberService.bulkAddMembersToProject(workspaceSlug, projectId, data).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((member) => {
|
||||
set(this.projectMemberMap, [projectId, member.member], {
|
||||
...member,
|
||||
role: this.getUserProjectRole(member.member, projectId) ?? member.role,
|
||||
original_role: member.role,
|
||||
});
|
||||
});
|
||||
});
|
||||
update(this.projectRoot.projectMap, [projectId, "members"], (memberIds) =>
|
||||
uniq([...memberIds, ...data.members.map((m) => m.member_id)])
|
||||
);
|
||||
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members?.concat(
|
||||
data.members.map((m) => m.member_id)
|
||||
);
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description update the role of a member in a project
|
||||
* @param projectId
|
||||
* @param userId
|
||||
* @param role
|
||||
*/
|
||||
abstract getProjectMemberRoleForUpdate: (
|
||||
projectId: string,
|
||||
userId: string,
|
||||
role: EUserProjectRoles
|
||||
) => EUserProjectRoles;
|
||||
|
||||
/**
|
||||
* @description update the role of a member in a project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param userId
|
||||
* @param data
|
||||
*/
|
||||
updateMemberRole = async (workspaceSlug: string, projectId: string, userId: string, role: EUserProjectRoles) => {
|
||||
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
||||
if (!memberDetails || !memberDetails?.id) throw new Error("Member not found");
|
||||
// original data to revert back in case of error
|
||||
const isCurrentUser = this.rootStore.user.data?.id === userId;
|
||||
const membershipBeforeUpdate = { ...this.getProjectMembershipByUserId(userId, projectId) };
|
||||
const permissionBeforeUpdate = isCurrentUser
|
||||
? this.rootStore.user.permission.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId)
|
||||
: undefined;
|
||||
const updatedProjectRole = this.getProjectMemberRoleForUpdate(projectId, userId, role);
|
||||
try {
|
||||
runInAction(() => {
|
||||
set(this.projectMemberMap, [projectId, userId, "original_role"], role);
|
||||
set(this.projectMemberMap, [projectId, userId, "role"], updatedProjectRole);
|
||||
if (isCurrentUser) {
|
||||
set(
|
||||
this.rootStore.user.permission.workspaceProjectsPermissions,
|
||||
[workspaceSlug, projectId],
|
||||
updatedProjectRole
|
||||
);
|
||||
}
|
||||
set(this.rootStore.user.permission.projectUserInfo, [workspaceSlug, projectId, "role"], updatedProjectRole);
|
||||
});
|
||||
const response = await this.projectMemberService.updateProjectMember(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
memberDetails?.id,
|
||||
{
|
||||
role,
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
// revert back to original members in case of error
|
||||
runInAction(() => {
|
||||
set(this.projectMemberMap, [projectId, userId, "original_role"], membershipBeforeUpdate?.original_role);
|
||||
set(this.projectMemberMap, [projectId, userId, "role"], membershipBeforeUpdate?.role);
|
||||
if (isCurrentUser) {
|
||||
set(
|
||||
this.rootStore.user.permission.workspaceProjectsPermissions,
|
||||
[workspaceSlug, projectId],
|
||||
membershipBeforeUpdate?.original_role
|
||||
);
|
||||
set(
|
||||
this.rootStore.user.permission.projectUserInfo,
|
||||
[workspaceSlug, projectId, "role"],
|
||||
permissionBeforeUpdate
|
||||
);
|
||||
}
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Handles the removal of a member from a project
|
||||
* @param projectId - The ID of the project to remove the member from
|
||||
* @param userId - The ID of the user to remove from the project
|
||||
*/
|
||||
protected handleMemberRemoval = (projectId: string, userId: string) => {
|
||||
unset(this.projectMemberMap, [projectId, userId]);
|
||||
set(
|
||||
this.projectRoot.projectMap,
|
||||
[projectId, "members"],
|
||||
this.projectRoot.projectMap?.[projectId]?.members?.filter((memberId) => memberId !== userId)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Processes the removal of a member from a project
|
||||
* This abstract method handles the cleanup of member data from the project member map
|
||||
* @param projectId - The ID of the project to remove the member from
|
||||
* @param userId - The ID of the user to remove from the project
|
||||
*/
|
||||
abstract processMemberRemoval: (projectId: string, userId: string) => void;
|
||||
|
||||
/**
|
||||
* @description remove a member from a project
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
* @param userId
|
||||
*/
|
||||
removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => {
|
||||
const memberDetails = this.getProjectMemberDetails(userId, projectId);
|
||||
if (!memberDetails || !memberDetails?.id) throw new Error("Member not found");
|
||||
await this.projectMemberService.deleteProjectMember(workspaceSlug, projectId, memberDetails?.id).then(() => {
|
||||
runInAction(() => {
|
||||
this.processMemberRemoval(projectId, userId);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import type { IUserLite, TProjectMembership } from "@plane/types";
|
||||
// local imports
|
||||
import type { IMemberFilters } from "../utils";
|
||||
import { sortProjectMembers } from "../utils";
|
||||
|
||||
export interface IProjectMemberFiltersStore {
|
||||
// observables
|
||||
filtersMap: Record<string, IMemberFilters>;
|
||||
// computed actions
|
||||
getFilteredMemberIds: (
|
||||
members: TProjectMembership[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: TProjectMembership) => string,
|
||||
projectId: string
|
||||
) => string[];
|
||||
// actions
|
||||
updateFilters: (projectId: string, filters: Partial<IMemberFilters>) => void;
|
||||
getFilters: (projectId: string) => IMemberFilters | undefined;
|
||||
}
|
||||
|
||||
export class ProjectMemberFiltersStore implements IProjectMemberFiltersStore {
|
||||
// observables
|
||||
filtersMap: Record<string, IMemberFilters> = {};
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
filtersMap: observable,
|
||||
// actions
|
||||
updateFilters: action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get filtered and sorted member ids
|
||||
* @param members - array of project membership objects
|
||||
* @param memberDetailsMap - map of member details by user id
|
||||
* @param getMemberKey - function to get member key from membership object
|
||||
* @param projectId - project id to get filters for
|
||||
*/
|
||||
getFilteredMemberIds = computedFn(
|
||||
(
|
||||
members: TProjectMembership[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: TProjectMembership) => string,
|
||||
projectId: string
|
||||
): string[] => {
|
||||
if (!members || members.length === 0) return [];
|
||||
|
||||
// Apply filters and sorting
|
||||
const sortedMembers = sortProjectMembers(members, memberDetailsMap, getMemberKey, this.filtersMap[projectId]);
|
||||
|
||||
return sortedMembers.map(getMemberKey);
|
||||
}
|
||||
);
|
||||
|
||||
getFilters = (projectId: string) => this.filtersMap[projectId];
|
||||
|
||||
/**
|
||||
* @description update filters
|
||||
* @param projectId - project id
|
||||
* @param filters - partial filters to update
|
||||
*/
|
||||
updateFilters = (projectId: string, filters: Partial<IMemberFilters>) => {
|
||||
const current = this.filtersMap[projectId] ?? {};
|
||||
this.filtersMap[projectId] = { ...current, ...filters };
|
||||
};
|
||||
}
|
||||
187
apps/web/core/store/member/utils.ts
Normal file
187
apps/web/core/store/member/utils.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
// Types and utilities for member filtering
|
||||
import type { EUserPermissions, TMemberOrderByOptions } from "@plane/constants";
|
||||
import type { IUserLite, TProjectMembership } from "@plane/types";
|
||||
|
||||
export interface IMemberFilters {
|
||||
order_by?: TMemberOrderByOptions;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
// Helper function to parse order key and direction
|
||||
export const parseOrderKey = (orderKey?: TMemberOrderByOptions): { field: string; direction: "asc" | "desc" } => {
|
||||
// Default to sorting by display_name in ascending order when no order key is provided
|
||||
if (!orderKey) {
|
||||
return {
|
||||
field: "display_name",
|
||||
direction: "asc",
|
||||
};
|
||||
}
|
||||
|
||||
const isDescending = orderKey.startsWith("-");
|
||||
const field = isDescending ? orderKey.slice(1) : orderKey;
|
||||
return {
|
||||
field,
|
||||
direction: isDescending ? "desc" : "asc",
|
||||
};
|
||||
};
|
||||
|
||||
// Unified function to get sort key for any member type
|
||||
export const getMemberSortKey = (memberDetails: IUserLite, field: string, memberRole?: string): string | Date => {
|
||||
switch (field) {
|
||||
case "display_name":
|
||||
return memberDetails.display_name?.toLowerCase() || "";
|
||||
case "full_name": {
|
||||
const firstName = memberDetails.first_name || "";
|
||||
const lastName = memberDetails.last_name || "";
|
||||
return `${firstName} ${lastName}`.toLowerCase().trim();
|
||||
}
|
||||
case "email":
|
||||
return memberDetails.email?.toLowerCase() || "";
|
||||
case "joining_date": {
|
||||
if (!memberDetails.joining_date) {
|
||||
// Return a very old date for missing dates to sort them last
|
||||
return new Date(0);
|
||||
}
|
||||
const date = new Date(memberDetails.joining_date);
|
||||
// Return a very old date for invalid dates to sort them last
|
||||
return isNaN(date.getTime()) ? new Date(0) : date;
|
||||
}
|
||||
case "role":
|
||||
return (memberRole ?? "").toString().toLowerCase();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Filter functions
|
||||
export const filterProjectMembersByRole = (
|
||||
members: TProjectMembership[],
|
||||
roleFilters: string[]
|
||||
): TProjectMembership[] => {
|
||||
if (roleFilters.length === 0) return members;
|
||||
|
||||
return members.filter((member) => {
|
||||
const memberRole = String(member.role ?? member.original_role ?? "");
|
||||
return roleFilters.includes(memberRole);
|
||||
});
|
||||
};
|
||||
|
||||
export const filterWorkspaceMembersByRole = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
|
||||
members: T[],
|
||||
roleFilters: string[]
|
||||
): T[] => {
|
||||
if (roleFilters.length === 0) return members;
|
||||
|
||||
return members.filter((member) => {
|
||||
const memberRole = String(member.role ?? "");
|
||||
const isSuspended = member.is_active === false;
|
||||
|
||||
// Check if suspended is in the role filters
|
||||
const hasSuspendedFilter = roleFilters.includes("suspended");
|
||||
// Get non-suspended role filters
|
||||
const activeRoleFilters = roleFilters.filter((role) => role !== "suspended");
|
||||
|
||||
// For suspended users, include them only if suspended filter is selected
|
||||
if (isSuspended) {
|
||||
return hasSuspendedFilter;
|
||||
}
|
||||
|
||||
// For active users, include them only if their role matches any active role filter
|
||||
return activeRoleFilters.includes(memberRole);
|
||||
});
|
||||
};
|
||||
|
||||
// Unified sorting function
|
||||
export const sortMembers = <T>(
|
||||
members: T[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: T) => string,
|
||||
getMemberRole: (member: T) => string,
|
||||
orderBy?: TMemberOrderByOptions
|
||||
): T[] => {
|
||||
if (!orderBy) return members;
|
||||
|
||||
const { field, direction } = parseOrderKey(orderBy);
|
||||
|
||||
return [...members].sort((a, b) => {
|
||||
const aKey = getMemberKey(a);
|
||||
const bKey = getMemberKey(b);
|
||||
const aMemberDetails = memberDetailsMap[aKey];
|
||||
const bMemberDetails = memberDetailsMap[bKey];
|
||||
|
||||
if (!aMemberDetails || !bMemberDetails) return 0;
|
||||
|
||||
const aRole = getMemberRole(a);
|
||||
const bRole = getMemberRole(b);
|
||||
|
||||
const aValue = getMemberSortKey(aMemberDetails, field, aRole);
|
||||
const bValue = getMemberSortKey(bMemberDetails, field, bRole);
|
||||
|
||||
let comparison = 0;
|
||||
|
||||
if (field === "joining_date") {
|
||||
// For dates, we need to handle Date objects and ensure they're valid
|
||||
const aDate = aValue instanceof Date ? aValue : new Date(aValue);
|
||||
const bDate = bValue instanceof Date ? bValue : new Date(bValue);
|
||||
|
||||
// Handle invalid dates by treating them as very old dates
|
||||
const aTime = isNaN(aDate.getTime()) ? 0 : aDate.getTime();
|
||||
const bTime = isNaN(bDate.getTime()) ? 0 : bDate.getTime();
|
||||
|
||||
comparison = aTime - bTime;
|
||||
} else {
|
||||
// For strings, use localeCompare for proper alphabetical sorting
|
||||
const aStr = String(aValue);
|
||||
const bStr = String(bValue);
|
||||
comparison = aStr.localeCompare(bStr);
|
||||
}
|
||||
|
||||
return direction === "desc" ? -comparison : comparison;
|
||||
});
|
||||
};
|
||||
|
||||
// Specific implementations using the unified functions
|
||||
export const sortProjectMembers = (
|
||||
members: TProjectMembership[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: TProjectMembership) => string,
|
||||
filters?: IMemberFilters
|
||||
): TProjectMembership[] => {
|
||||
// Apply role filtering first
|
||||
const filteredMembers =
|
||||
filters?.roles && filters.roles.length > 0 ? filterProjectMembersByRole(members, filters.roles) : members;
|
||||
|
||||
// If no order_by filter, return filtered members
|
||||
if (!filters?.order_by) return filteredMembers;
|
||||
|
||||
// Apply sorting
|
||||
return sortMembers(
|
||||
filteredMembers,
|
||||
memberDetailsMap,
|
||||
getMemberKey,
|
||||
(member) => String(member.role ?? member.original_role ?? ""),
|
||||
filters.order_by
|
||||
);
|
||||
};
|
||||
|
||||
export const sortWorkspaceMembers = <T extends { role: string | EUserPermissions; is_active?: boolean }>(
|
||||
members: T[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: T) => string,
|
||||
filters?: IMemberFilters
|
||||
): T[] => {
|
||||
const filteredMembers =
|
||||
filters?.roles && filters.roles.length > 0 ? filterWorkspaceMembersByRole(members, filters.roles) : members;
|
||||
|
||||
// If no order_by filter, return filtered members
|
||||
if (!filters?.order_by) return filteredMembers;
|
||||
|
||||
// Apply sorting
|
||||
return sortMembers(
|
||||
filteredMembers,
|
||||
memberDetailsMap,
|
||||
getMemberKey,
|
||||
(member) => String(member.role ?? ""),
|
||||
filters.order_by
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import type { EUserPermissions } from "@plane/constants";
|
||||
import type { IUserLite } from "@plane/types";
|
||||
// local imports
|
||||
import type { IMemberFilters } from "../utils";
|
||||
import { sortWorkspaceMembers } from "../utils";
|
||||
|
||||
// Workspace membership interface matching the store structure
|
||||
interface IWorkspaceMembership {
|
||||
id: string;
|
||||
member: string;
|
||||
role: EUserPermissions;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceMemberFiltersStore {
|
||||
// observables
|
||||
filters: IMemberFilters;
|
||||
// computed actions
|
||||
getFilteredMemberIds: (
|
||||
members: IWorkspaceMembership[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: IWorkspaceMembership) => string
|
||||
) => string[];
|
||||
// actions
|
||||
updateFilters: (filters: Partial<IMemberFilters>) => void;
|
||||
}
|
||||
|
||||
export class WorkspaceMemberFiltersStore implements IWorkspaceMemberFiltersStore {
|
||||
// observables
|
||||
filters: IMemberFilters = {};
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
filters: observable,
|
||||
// actions
|
||||
updateFilters: action,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description get filtered and sorted member ids
|
||||
* @param members - array of workspace membership objects
|
||||
* @param memberDetailsMap - map of member details by user id
|
||||
* @param getMemberKey - function to get member key from membership object
|
||||
*/
|
||||
getFilteredMemberIds = computedFn(
|
||||
(
|
||||
members: IWorkspaceMembership[],
|
||||
memberDetailsMap: Record<string, IUserLite>,
|
||||
getMemberKey: (member: IWorkspaceMembership) => string
|
||||
): string[] => {
|
||||
if (!members || members.length === 0) return [];
|
||||
|
||||
// Apply filters and sorting
|
||||
const sortedMembers = sortWorkspaceMembers(members, memberDetailsMap, getMemberKey, this.filters);
|
||||
|
||||
return sortedMembers.map(getMemberKey);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description update filters
|
||||
* @param filters - partial filters to update
|
||||
*/
|
||||
updateFilters = (filters: Partial<IMemberFilters>) => {
|
||||
this.filters = { ...this.filters, ...filters };
|
||||
};
|
||||
}
|
||||
359
apps/web/core/store/member/workspace/workspace-member.store.ts
Normal file
359
apps/web/core/store/member/workspace/workspace-member.store.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import { set, sortBy } from "lodash-es";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import type { EUserPermissions } from "@plane/constants";
|
||||
import type { IWorkspaceBulkInviteFormData, IWorkspaceMember, IWorkspaceMemberInvitation } from "@plane/types";
|
||||
// plane-web constants
|
||||
// services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// types
|
||||
import type { IRouterStore } from "@/store/router.store";
|
||||
import type { IUserStore } from "@/store/user";
|
||||
// store
|
||||
import type { CoreRootStore } from "../../root.store";
|
||||
import type { IMemberRootStore } from "../index.ts";
|
||||
import type { IWorkspaceMemberFiltersStore } from "./workspace-member-filters.store";
|
||||
import { WorkspaceMemberFiltersStore } from "./workspace-member-filters.store";
|
||||
|
||||
export interface IWorkspaceMembership {
|
||||
id: string;
|
||||
member: string;
|
||||
role: EUserPermissions;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkspaceMemberStore {
|
||||
// observables
|
||||
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
|
||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]>;
|
||||
// filters store
|
||||
filtersStore: IWorkspaceMemberFiltersStore;
|
||||
// computed
|
||||
workspaceMemberIds: string[] | null;
|
||||
workspaceMemberInvitationIds: string[] | null;
|
||||
memberMap: Record<string, IWorkspaceMembership> | null;
|
||||
// computed actions
|
||||
getWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||
getFilteredWorkspaceMemberIds: (workspaceSlug: string) => string[];
|
||||
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<IWorkspaceMember[]>;
|
||||
fetchWorkspaceMemberInvitations: (workspaceSlug: string) => Promise<IWorkspaceMemberInvitation[]>;
|
||||
// crud actions
|
||||
updateMember: (workspaceSlug: string, userId: string, data: { role: EUserPermissions }) => Promise<void>;
|
||||
removeMemberFromWorkspace: (workspaceSlug: string, userId: string) => Promise<void>;
|
||||
// invite actions
|
||||
inviteMembersToWorkspace: (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => Promise<void>;
|
||||
updateMemberInvitation: (
|
||||
workspaceSlug: string,
|
||||
invitationId: string,
|
||||
data: Partial<IWorkspaceMemberInvitation>
|
||||
) => Promise<void>;
|
||||
deleteMemberInvitation: (workspaceSlug: string, invitationId: string) => Promise<void>;
|
||||
isUserSuspended: (userId: string, workspaceSlug: string) => boolean;
|
||||
}
|
||||
|
||||
export class WorkspaceMemberStore implements IWorkspaceMemberStore {
|
||||
// observables
|
||||
workspaceMemberMap: {
|
||||
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
|
||||
} = {}; // { workspaceSlug: { userId: userDetails } }
|
||||
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]> = {}; // { workspaceSlug: [invitations] }
|
||||
// filters store
|
||||
filtersStore: IWorkspaceMemberFiltersStore;
|
||||
// stores
|
||||
routerStore: IRouterStore;
|
||||
userStore: IUserStore;
|
||||
memberRoot: IMemberRootStore;
|
||||
// services
|
||||
workspaceService;
|
||||
|
||||
constructor(_memberRoot: IMemberRootStore, _rootStore: CoreRootStore) {
|
||||
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,
|
||||
});
|
||||
// initialize filters store
|
||||
this.filtersStore = new WorkspaceMemberFiltersStore();
|
||||
// root store
|
||||
this.routerStore = _rootStore.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;
|
||||
|
||||
return this.getWorkspaceMemberIds(workspaceSlug);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
getWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
members = sortBy(members, [
|
||||
(m) => m.member !== this.userStore?.data?.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;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description get the filtered and sorted list of all the user ids of all the members of the workspace
|
||||
* @param workspaceSlug
|
||||
*/
|
||||
getFilteredWorkspaceMemberIds = computedFn((workspaceSlug: string) => {
|
||||
let members = Object.values(this.workspaceMemberMap?.[workspaceSlug] ?? {});
|
||||
//filter out bots and inactive members
|
||||
members = members.filter((m) => !this.memberRoot?.memberMap?.[m.member]?.is_bot);
|
||||
|
||||
// Use filters store to get filtered member ids
|
||||
const memberIds = this.filtersStore.getFilteredMemberIds(
|
||||
members,
|
||||
this.memberRoot?.memberMap || {},
|
||||
(member) => member.member
|
||||
);
|
||||
|
||||
return memberIds;
|
||||
});
|
||||
|
||||
/**
|
||||
* @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 filteredMemberIds = this.getFilteredWorkspaceMemberIds(workspaceSlug);
|
||||
if (!filteredMemberIds) return null;
|
||||
const searchedWorkspaceMemberIds = filteredMemberIds.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],
|
||||
is_active: workspaceMember.is_active,
|
||||
};
|
||||
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, joining_date: member.created_at });
|
||||
set(this.workspaceMemberMap, [workspaceSlug, member.member.id], {
|
||||
id: member.id,
|
||||
member: member.member.id,
|
||||
role: member.role,
|
||||
is_active: member.is_active,
|
||||
});
|
||||
});
|
||||
});
|
||||
return response;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description update the role of a workspace member
|
||||
* @param workspaceSlug
|
||||
* @param userId
|
||||
* @param data
|
||||
*/
|
||||
updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserPermissions }) => {
|
||||
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(() => {
|
||||
set(this.workspaceMemberMap, [workspaceSlug, userId, "is_active"], false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @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<IWorkspaceMemberInvitation>
|
||||
) => {
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
isUserSuspended = computedFn((userId: string, workspaceSlug: string) => {
|
||||
if (!workspaceSlug) return false;
|
||||
const workspaceMember = this.workspaceMemberMap?.[workspaceSlug]?.[userId];
|
||||
return workspaceMember?.is_active === false;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user