Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
} = useMember();
const { currentWorkspace } = useWorkspace();
const { currentWorkspace, mutateWorkspaceMembersActivity } = useWorkspace();
const { t } = useTranslation();

// derived values
Expand All @@ -55,6 +55,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
try {
await inviteMembersToWorkspace(workspaceSlug, data);
void mutateWorkspaceMembersActivity(workspaceSlug);

setInviteModal(false);

Expand Down
18 changes: 18 additions & 0 deletions apps/web/ce/store/workspace/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// store
import { BaseWorkspaceRootStore } from "@/store/workspace";
import type { RootStore } from "@/plane-web/store/root.store";

export class WorkspaceRootStore extends BaseWorkspaceRootStore {
constructor(_rootStore: RootStore) {
super(_rootStore);
}

// actions
/**
* Mutate workspace members activity
* @param workspaceSlug
*/
mutateWorkspaceMembersActivity = async (_workspaceSlug: string) => {
// No-op in default/CE version
};
}
Comment on lines +1 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find BaseWorkspaceRootStore definition
rg "class BaseWorkspaceRootStore" -A 20

Repository: makeplane/plane

Length of output: 174


🏁 Script executed:

#!/bin/bash
# Also search for the file location to understand the import path
fd "BaseWorkspaceRootStore" -type f

Repository: makeplane/plane

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Check the store directory structure
git ls-files apps/web/store/workspace/ | head -20

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Try to read the file directly
cat apps/web/ce/store/workspace/index.ts

Repository: makeplane/plane

Length of output: 526


🏁 Script executed:

#!/bin/bash
# Search for BaseWorkspaceRootStore using simple pattern
rg "BaseWorkspaceRootStore" --context 5

Repository: makeplane/plane

Length of output: 1476


🏁 Script executed:

#!/bin/bash
# Check if the import path exists
cat apps/web/store/workspace/index.ts 2>/dev/null | head -50

Repository: makeplane/plane

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Get more context on BaseWorkspaceRootStore including mutateWorkspaceMembersActivity
rg "mutateWorkspaceMembersActivity" --context 10

Repository: makeplane/plane

Length of output: 20326


🏁 Script executed:

#!/bin/bash
# Check the full BaseWorkspaceRootStore class definition
cat apps/web/core/store/workspace/index.ts | head -100

Repository: makeplane/plane

Length of output: 3697


🏁 Script executed:

#!/bin/bash
# Check for explicit return type annotations in the base class
rg "mutateWorkspaceMembersActivity.*=" -A 3

Repository: makeplane/plane

Length of output: 2416


🏁 Script executed:

#!/bin/bash
# Verify if there are explicit return type annotations elsewhere in the store
rg "= async \(" apps/web/core/store/workspace/ -A 2 | head -30

Repository: makeplane/plane

Length of output: 2081


🏁 Script executed:

#!/bin/bash
# Check TypeScript configuration to confirm strict mode is enabled
cat tsconfig.json | grep -A 5 '"strict"'

Repository: makeplane/plane

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Find tsconfig files
find . -name "tsconfig*.json" -type f | head -5

Repository: makeplane/plane

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Check the coding guidelines compliance by looking at other store implementations in CE
find apps/web/ce -name "*.ts" -type f | xargs cat | head -100

Repository: makeplane/plane

Length of output: 3645


🏁 Script executed:

#!/bin/bash
# Verify the method signature in the CE implementation matches the abstract signature
rg "abstract mutateWorkspaceMembersActivity|mutateWorkspaceMembersActivity = async" -B 2 -A 5

Repository: makeplane/plane

Length of output: 699


Add explicit return type annotation for strict TypeScript compliance.

The CE store correctly implements the no-op pattern and extends BaseWorkspaceRootStore. However, the mutateWorkspaceMembersActivity method lacks an explicit return type annotation. For consistency with the abstract method signature and to comply with strict TypeScript mode, add : Promise<void>:

mutateWorkspaceMembersActivity = async (_workspaceSlug: string): Promise<void> => {
  // No-op in default/CE version
};
🤖 Prompt for AI Agents
In apps/web/ce/store/workspace/index.ts around lines 1 to 18, the async no-op
method mutateWorkspaceMembersActivity lacks an explicit return type which
violates strict TypeScript and the abstract signature; update its declaration to
include the Promise<void> return type (i.e., make it async (_workspaceSlug:
string): Promise<void> => ...) so it matches the base/abstract method and
satisfies strict typing while keeping the no-op body unchanged.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-wor
import { captureClick } from "@/helpers/event-tracker.helper";
import { useMember } from "@/hooks/store/use-member";
import { useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";

type Props = {
invitationId: string;
Expand All @@ -31,6 +32,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
const { t } = useTranslation();
// store hooks
const { allowPermissions, workspaceInfoBySlug } = useUserPermissions();
const { mutateWorkspaceMembersActivity } = useWorkspace();
const {
workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails },
} = useMember();
Expand All @@ -50,36 +52,36 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
);

const handleRemoveInvitation = async () => {
if (!workspaceSlug || !invitationDetails) return;
try {
if (!workspaceSlug || !invitationDetails) return;

await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Invitation removed successfully.",
});
})
.catch((err) =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error || "Something went wrong. Please try again.",
})
);
await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Invitation removed successfully.",
});
void mutateWorkspaceMembersActivity(workspaceSlug);
} catch (err: unknown) {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error || "Something went wrong. Please try again.",
});
}
};

if (!invitationDetails || !currentWorkspaceMemberInfo) return null;

const handleCopyText = () => {
const handleCopyText = async () => {
try {
const inviteLink = new URL(invitationDetails.invite_link, window.location.origin).href;
copyTextToClipboard(inviteLink).then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }),
});
await copyTextToClipboard(inviteLink);
setToast({
type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }),
});
} catch (error) {
console.error("Error generating invite link:", error);
Expand All @@ -89,7 +91,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
const MENU_ITEMS: TContextMenuItem[] = [
{
key: "copy-link",
action: handleCopyText,
action: () => void handleCopyText(),
title: t("common.actions.copy_link"),
icon: LinkIcon,
shouldRender: !!invitationDetails.invite_link,
Expand Down Expand Up @@ -157,7 +159,8 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio

updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, {
role: value,
}).catch((error) => {
}).catch((err: unknown) => {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
Expand All @@ -169,7 +172,11 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
placement="bottom-end"
>
{Object.keys(ROLE).map((key) => {
if (currentWorkspaceRole && currentWorkspaceRole !== 20 && currentWorkspaceRole < parseInt(key))
if (
currentWorkspaceRole &&
Number(currentWorkspaceRole) !== 20 &&
Number(currentWorkspaceRole) < parseInt(key)
)
return null;

return (
Expand Down
32 changes: 22 additions & 10 deletions apps/web/core/components/workspace/settings/member-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getFileURL } from "@plane/utils";
// hooks
import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
// plane web constants

export interface RowData {
Expand Down Expand Up @@ -45,7 +46,7 @@ export function NameColumn(props: NameProps) {

return (
<Disclosure>
{({}) => (
{() => (
<div className="relative group">
<div className="flex items-center gap-x-4 gap-y-2 w-72 justify-between">
<div className="flex items-center gap-x-2 gap-y-2 flex-1">
Expand Down Expand Up @@ -83,8 +84,16 @@ export function NameColumn(props: NameProps) {
buttonClassName="outline-none origin-center rotate-90 size-8 aspect-square flex-shrink-0 grid place-items-center opacity-0 group-hover:opacity-100 transition-opacity"
render={() => (
<div
role="button"
tabIndex={0}
className="flex items-center gap-x-3 cursor-pointer"
onClick={() => setRemoveMemberModal(rowData)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setRemoveMemberModal(rowData);
}
}}
data-ph-element={MEMBER_TRACKER_ELEMENTS.WORKSPACE_MEMBER_TABLE_CONTEXT_MENU}
>
<Trash2 className="size-3.5 align-middle" /> {id === currentUser?.id ? "Leave " : "Remove "}
Expand Down Expand Up @@ -112,6 +121,7 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
const {
workspace: { updateMember },
} = useMember();
const { mutateWorkspaceMembersActivity } = useWorkspace();
const { data: currentUser } = useUser();

// derived values
Expand Down Expand Up @@ -139,22 +149,24 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
rules={{ required: "Role is required." }}
render={({ field: { value } }) => (
<CustomSelect
value={value}
onChange={(value: EUserPermissions) => {
value={value as EUserPermissions}
onChange={async (value: EUserPermissions) => {
if (!workspaceSlug) return;
updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions
}).catch((err) => {
console.log(err, "err");
const error = err.error;
const errorString = Array.isArray(error) ? error[0] : error;
try {
await updateMember(workspaceSlug.toString(), rowData.member.id, {
role: value as unknown as EUserPermissions,
});
void mutateWorkspaceMembersActivity(workspaceSlug);
} catch (err: unknown) {
const error = err as { error?: string | string[] };
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;

setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.",
});
});
}
}}
label={
<div className="flex ">
Expand Down
70 changes: 39 additions & 31 deletions apps/web/core/components/workspace/settings/members-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Table } from "@plane/ui";
// components
import { MembersLayoutLoader } from "@/components/ui/loader/layouts/members-layout-loader";
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
import type { RowData } from "@/components/workspace/settings/member-columns";
// helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks
Expand All @@ -34,51 +35,56 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
workspace: { removeMemberFromWorkspace },
} = useMember();
const { leaveWorkspace } = useUserPermissions();
const { getWorkspaceRedirectionUrl } = useWorkspace();
const { getWorkspaceRedirectionUrl, mutateWorkspaceMembersActivity } = useWorkspace();
const { fetchCurrentUserSettings } = useUserSettings();
const { t } = useTranslation();
// derived values

const handleLeaveWorkspace = async () => {
if (!workspaceSlug || !currentUser) return;

await leaveWorkspace(workspaceSlug.toString())
.then(async () => {
await fetchCurrentUserSettings();
router.push(getWorkspaceRedirectionUrl());
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
});
})
.catch((err: any) => {
captureError({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
error: err,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error || t("something_went_wrong_please_try_again"),
});
try {
await leaveWorkspace(workspaceSlug.toString());
await fetchCurrentUserSettings();
router.push(getWorkspaceRedirectionUrl());
captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
});
} catch (err: unknown) {
const error = err as { error?: string };
const errorForCapture: Error | string = err instanceof Error ? err : String(err);
captureError({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: {
workspace: workspaceSlug,
},
error: errorForCapture,
});
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: error?.error || t("something_went_wrong_please_try_again"),
});
}
};

const handleRemoveMember = async (memberId: string) => {
if (!workspaceSlug || !memberId) return;

await removeMemberFromWorkspace(workspaceSlug.toString(), memberId).catch((err) =>
try {
await removeMemberFromWorkspace(workspaceSlug.toString(), memberId);
void mutateWorkspaceMembersActivity(workspaceSlug);
} catch (err: unknown) {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error || t("something_went_wrong_please_try_again"),
})
);
message: error?.error || t("something_went_wrong_please_try_again"),
});
}
};

const handleRemove = async (memberId: string) => {
Expand Down Expand Up @@ -109,9 +115,11 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
onSubmit={() => handleRemove(removeMemberModal.member.id)}
/>
)}
<Table
<Table<RowData>
columns={columns ?? []}
data={(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as any}
data={
(memberDetails?.filter((member): member is IWorkspaceMember => member !== null) ?? []) as unknown as RowData[]
}
Comment on lines +118 to +122
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find RowData type definition
rg -n "type RowData|interface RowData" apps/web/core/components/workspace/settings/ -A3

Repository: makeplane/plane

Length of output: 425


🏁 Script executed:

# Check member-columns.tsx to see how columns access the data
rg -n "rowData\.|data\[|member" apps/web/core/components/workspace/settings/member-columns.tsx -B1 -A1

Repository: makeplane/plane

Length of output: 1396


🏁 Script executed:

# Check the memberDetails type and structure
rg -n "memberDetails" apps/web/core/components/workspace/settings/members-list-item.tsx -B2 -A2

Repository: makeplane/plane

Length of output: 857


🏁 Script executed:

# Look at how Table component is typed and what it expects
rg -n "Table<" apps/web/core/components/workspace/settings/members-list-item.tsx -B2 -A5

Repository: makeplane/plane

Length of output: 371


🏁 Script executed:

# Find IWorkspaceMember interface definition
rg -n "interface IWorkspaceMember|type IWorkspaceMember" --type=ts -A10

Repository: makeplane/plane

Length of output: 5641


🏁 Script executed:

# Also check imports in members-list-item to see full picture
head -30 apps/web/core/components/workspace/settings/members-list-item.tsx

Repository: makeplane/plane

Length of output: 1469


Transform the memberDetails array into the correct RowData shape instead of using the double cast.

The IWorkspaceMember objects are missing the is_active property that RowData expects. The columns access rowData.is_active at runtime, which will fail. Transform each member object into { member: IWorkspaceMember; role: EUserPermissions; is_active: boolean } before passing to the Table component, rather than casting to bypass type checking.

🤖 Prompt for AI Agents
In apps/web/core/components/workspace/settings/members-list-item.tsx around
lines 118–122, stop double-casting memberDetails to RowData and instead
map/filter the array to produce the exact RowData shape: for each non-null
IWorkspaceMember return an object { member: theMember, role: theMember.role ??
EUserPermissions.Member, is_active: Boolean(theMember.is_active ??
theMember.active ?? true) } (or another sensible default for role/is_active
based on your domain), then pass that mapped array to Table; this ensures the
Table columns can safely access rowData.is_active without runtime type errors.

keyExtractor={(rowData) => rowData?.member.id ?? ""}
tHeadClassName="border-b border-subtle"
thClassName="text-left font-medium divide-x-0 text-placeholder"
Expand Down
6 changes: 3 additions & 3 deletions apps/web/core/store/root.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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 @@ -61,7 +62,6 @@ 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 Expand Up @@ -102,7 +102,7 @@ export class CoreRootStore {
this.instance = new InstanceStore();
this.user = new UserStore(this as unknown as RootStore);
this.theme = new ThemeStore();
this.workspaceRoot = new WorkspaceRootStore(this);
this.workspaceRoot = new WorkspaceRootStore(this as unknown as RootStore);
this.projectRoot = new ProjectRootStore(this);
this.memberRoot = new MemberRootStore(this as unknown as RootStore);
this.cycle = new CycleStore(this);
Expand Down Expand Up @@ -136,7 +136,7 @@ export class CoreRootStore {
this.commandPalette = new CommandPaletteStore();
this.instance = new InstanceStore();
this.user = new UserStore(this as unknown as RootStore);
this.workspaceRoot = new WorkspaceRootStore(this);
this.workspaceRoot = new WorkspaceRootStore(this as unknown as RootStore);
this.projectRoot = new ProjectRootStore(this);
this.memberRoot = new MemberRootStore(this as unknown as RootStore);
this.cycle = new CycleStore(this);
Expand Down
Loading
Loading