Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions apps/web/ce/store/member/project-member.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,4 @@ export class ProjectMemberStore extends BaseProjectMemberStore implements IProje
* @param userId - The ID of the user to remove from the project
*/
processMemberRemoval = (projectId: string, userId: string) => this.handleMemberRemoval(projectId, userId);

/**
* @description Mutate project members activity
* @param workspaceSlug
* @param projectId
*/
mutateProjectMembersActivity = async (_workspaceSlug: string, _projectId: string) => {
// No-op in default/CE version
};
}
13 changes: 13 additions & 0 deletions apps/web/ce/store/member/workspace-member.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// store
import type { IBaseWorkspaceMemberStore } from "@/store/member/workspace/workspace-member.store";
import { BaseWorkspaceMemberStore } from "@/store/member/workspace/workspace-member.store";
import type { RootStore } from "@/plane-web/store/root.store";
import type { IMemberRootStore } from "@/store/member";

export type IWorkspaceMemberStore = IBaseWorkspaceMemberStore;

export class WorkspaceMemberStore extends BaseWorkspaceMemberStore implements IWorkspaceMemberStore {
constructor(_memberRoot: IMemberRootStore, _rootStore: RootStore) {
super(_memberRoot, _rootStore);
}
}
18 changes: 0 additions & 18 deletions apps/web/ce/store/workspace/index.ts

This file was deleted.

5 changes: 2 additions & 3 deletions apps/web/core/store/member/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ 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 { IWorkspaceMemberStore } from "@/plane-web/store/member/workspace-member.store";
import { WorkspaceMemberStore } from "@/plane-web/store/member/workspace-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
Expand Down
26 changes: 9 additions & 17 deletions apps/web/core/store/member/project/base-project-member.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export interface IBaseProjectMemberStore {
role: EUserProjectRoles
) => Promise<TProjectMembership>;
removeMemberFromProject: (workspaceSlug: string, projectId: string, userId: string) => Promise<void>;
mutateProjectMembersActivity: (workspaceSlug: string, projectId: string) => Promise<void>;
}

export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore {
Expand Down Expand Up @@ -305,8 +304,8 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
* @param data
* @returns Promise<TProjectMembership[]>
*/
bulkAddMembersToProject = async (workspaceSlug: string, projectId: string, data: IProjectBulkAddFormData) =>
await this.projectMemberService.bulkAddMembersToProject(workspaceSlug, projectId, data).then((response) => {
async bulkAddMembersToProject(workspaceSlug: string, projectId: string, data: IProjectBulkAddFormData) {
return await this.projectMemberService.bulkAddMembersToProject(workspaceSlug, projectId, data).then((response) => {
runInAction(() => {
response.forEach((member) => {
set(this.projectMemberMap, [projectId, member.member], {
Expand All @@ -322,9 +321,10 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
this.projectRoot.projectMap[projectId].members = this.projectRoot.projectMap?.[projectId]?.members?.concat(
data.members.map((m) => m.member_id)
);
void this.mutateProjectMembersActivity(workspaceSlug, projectId);

return response;
});
}

/**
* @description update the role of a member in a project
Expand All @@ -345,7 +345,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
* @param userId
* @param data
*/
updateMemberRole = async (workspaceSlug: string, projectId: string, userId: string, role: EUserProjectRoles) => {
async updateMemberRole(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
Expand Down Expand Up @@ -376,7 +376,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
role,
}
);
void this.mutateProjectMembersActivity(workspaceSlug, projectId);

return response;
} catch (error) {
// revert back to original members in case of error
Expand All @@ -398,7 +398,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
});
throw error;
}
};
}

/**
* @description Handles the removal of a member from a project
Expand Down Expand Up @@ -428,15 +428,14 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
* @param projectId
* @param userId
*/
removeMemberFromProject = async (workspaceSlug: string, projectId: string, userId: string) => {
async removeMemberFromProject(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);
runInAction(() => {
this.processMemberRemoval(projectId, userId);
});
void this.mutateProjectMembersActivity(workspaceSlug, projectId);
};
}

/**
* @description get project member preferences
Expand Down Expand Up @@ -503,11 +502,4 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
throw error;
}
};

/**
* Mutate project members activity
* @param workspaceSlug
* @param projectId
*/
abstract mutateProjectMembersActivity(workspaceSlug: string, projectId: string): Promise<void>;
}
24 changes: 10 additions & 14 deletions apps/web/core/store/member/workspace/workspace-member.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface IWorkspaceMembership {
is_active?: boolean;
}

export interface IWorkspaceMemberStore {
export interface IBaseWorkspaceMemberStore {
// observables
workspaceMemberMap: Record<string, Record<string, IWorkspaceMembership>>;
workspaceMemberInvitations: Record<string, IWorkspaceMemberInvitation[]>;
Expand Down Expand Up @@ -57,7 +57,7 @@ export interface IWorkspaceMemberStore {
isUserSuspended: (userId: string, workspaceSlug: string) => boolean;
}

export class WorkspaceMemberStore implements IWorkspaceMemberStore {
export abstract class BaseWorkspaceMemberStore implements IBaseWorkspaceMemberStore {
// observables
workspaceMemberMap: {
[workspaceSlug: string]: Record<string, IWorkspaceMembership>;
Expand Down Expand Up @@ -251,7 +251,7 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @param userId
* @param data
*/
updateMember = async (workspaceSlug: string, userId: string, data: { role: EUserPermissions }) => {
async updateMember(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
Expand All @@ -261,30 +261,28 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
set(this.workspaceMemberMap, [workspaceSlug, userId, "role"], data.role);
});
await this.workspaceService.updateWorkspaceMember(workspaceSlug, memberDetails.id, data);
void this.rootStore.workspaceRoot.mutateWorkspaceMembersActivity(workspaceSlug);
} catch (error) {
// revert back to original members in case of error
runInAction(() => {
set(this.workspaceMemberMap, [workspaceSlug, userId], originalProjectMemberData);
});
throw error;
}
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods lose this binding after arrow-to-method conversion

Several methods (updateMember, removeMemberFromWorkspace, inviteMembersToWorkspace, deleteMemberInvitation, bulkAddMembersToProject, updateMemberRole, removeMemberFromProject) were converted from arrow functions to regular class methods. Throughout the codebase, these methods are destructured from stores and called as standalone functions (e.g., const { removeMemberFromProject } = useMember().project; await removeMemberFromProject(...)). Regular methods lose their this binding when destructured, causing this to be undefined at runtime. This will crash when users try to update roles, remove members, or invite members.

Additional Locations (2)

Fix in Cursor Fix in Web


/**
* @description remove a member from workspace
* @param workspaceSlug
* @param userId
*/
removeMemberFromWorkspace = async (workspaceSlug: string, userId: string) => {
async removeMemberFromWorkspace(workspaceSlug: string, userId: string) {
const memberDetails = this.getWorkspaceMemberDetails(userId);
if (!memberDetails) throw new Error("Member not found");
await this.workspaceService.deleteWorkspaceMember(workspaceSlug, memberDetails?.id);
runInAction(() => {
set(this.workspaceMemberMap, [workspaceSlug, userId, "is_active"], false);
});
void this.rootStore.workspaceRoot.mutateWorkspaceMembersActivity(workspaceSlug);
};
}

/**
* @description fetch all the member invitations of a workspace
Expand All @@ -303,11 +301,10 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @param workspaceSlug
* @param data
*/
inviteMembersToWorkspace = async (workspaceSlug: string, data: IWorkspaceBulkInviteFormData) => {
async inviteMembersToWorkspace(workspaceSlug: string, data: IWorkspaceBulkInviteFormData) {
await this.workspaceService.inviteWorkspace(workspaceSlug, data);
await this.fetchWorkspaceMemberInvitations(workspaceSlug);
void this.rootStore.workspaceRoot.mutateWorkspaceMembersActivity(workspaceSlug);
};
}
Comment on lines +304 to +307
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing MobX action decorator for inviteMembersToWorkspace.

The inviteMembersToWorkspace method was converted from an arrow function to a standard async method, but it's not included in the makeObservable configuration (lines 77-92). Since this method triggers state changes by calling fetchWorkspaceMemberInvitations, it should be decorated with action.

🔎 Proposed fix

Add the action decorator in the makeObservable call:

 makeObservable(this, {
   // observables
   workspaceMemberMap: observable,
   workspaceMemberInvitations: observable,
   // computed
   workspaceMemberIds: computed,
   workspaceMemberInvitationIds: computed,
   memberMap: computed,
   // actions
   fetchWorkspaceMembers: action,
   updateMember: action,
   removeMemberFromWorkspace: action,
   fetchWorkspaceMemberInvitations: action,
+  inviteMembersToWorkspace: action,
   updateMemberInvitation: action,
   deleteMemberInvitation: action,
 });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/core/store/member/workspace/workspace-member.store.ts around lines
304-307, the async method inviteMembersToWorkspace was converted from an arrow
function but not added to the makeObservable configuration (lines 77-92), so it
lacks the MobX action decorator; update the makeObservable call to include
inviteMembersToWorkspace as an action (alongside the other action-decorated
methods) so state changes triggered by fetchWorkspaceMemberInvitations are
wrapped as a MobX action.


/**
* @description update the role of a member invitation
Expand Down Expand Up @@ -345,15 +342,14 @@ export class WorkspaceMemberStore implements IWorkspaceMemberStore {
* @param workspaceSlug
* @param memberId
*/
deleteMemberInvitation = async (workspaceSlug: string, invitationId: string) => {
async deleteMemberInvitation(workspaceSlug: string, invitationId: string) {
await this.workspaceService.deleteWorkspaceInvitations(workspaceSlug.toString(), invitationId);
runInAction(() => {
this.workspaceMemberInvitations[workspaceSlug] = this.workspaceMemberInvitations[workspaceSlug].filter(
(inv) => inv.id !== invitationId
);
});
void this.rootStore.workspaceRoot.mutateWorkspaceMembersActivity(workspaceSlug);
};
}

isUserSuspended = computedFn((userId: string, workspaceSlug: string) => {
if (!workspaceSlug) return false;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/core/store/root.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type { IPowerKStore } from "@/plane-web/store/power-k.store";
import type { RootStore } from "@/plane-web/store/root.store";
import type { IStateStore } from "@/plane-web/store/state.store";
import { StateStore } from "@/plane-web/store/state.store";
import { WorkspaceRootStore } from "@/plane-web/store/workspace";
// stores
import type { ICycleStore } from "./cycle.store";
import { CycleStore } from "./cycle.store";
Expand Down Expand Up @@ -62,6 +61,7 @@ import { ThemeStore } from "./theme.store";
import type { IUserStore } from "./user";
import { UserStore } from "./user";
import type { IWorkspaceRootStore } from "./workspace";
import { WorkspaceRootStore } from "./workspace";

enableStaticRendering(typeof window === "undefined");

Expand Down
9 changes: 1 addition & 8 deletions apps/web/core/store/workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,13 @@ export interface IWorkspaceRootStore {
workspaceSlug: string,
data: Partial<IWorkspaceUserPropertiesResponse>
) => Promise<void>;
mutateWorkspaceMembersActivity: (workspaceSlug: string) => Promise<void>;
// sub-stores
webhook: IWebhookStore;
apiToken: IApiTokenStore;
home: IHomeStore;
}

export abstract class BaseWorkspaceRootStore implements IWorkspaceRootStore {
export class WorkspaceRootStore implements IWorkspaceRootStore {
loader: boolean = false;
// observables
workspaces: Record<string, IWorkspace> = {};
Expand Down Expand Up @@ -375,10 +374,4 @@ export abstract class BaseWorkspaceRootStore implements IWorkspaceRootStore {
throw error;
}
};

/**
* Mutate workspace members activity
* @param workspaceSlug
*/
abstract mutateWorkspaceMembersActivity(workspaceSlug: string): Promise<void>;
}
Loading