chore: settings workspace members enchancements

This commit is contained in:
b-saikrishnakanth
2025-12-15 19:58:25 +05:30
parent 22339b9786
commit 48e30f76d7
7 changed files with 133 additions and 76 deletions

View File

@@ -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
@@ -55,6 +55,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
try {
await inviteMembersToWorkspace(workspaceSlug, data);
void mutateWorkspaceMembersActivity(workspaceSlug);
setInviteModal(false);

View File

@@ -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
};
}

View File

@@ -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;
@@ -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();
@@ -50,37 +52,37 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
);
const handleRemoveInvitation = async () => {
try {
if (!workspaceSlug || !invitationDetails) return;
await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id)
.then(() => {
await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id);
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Invitation removed successfully.",
});
})
.catch((err) =>
void mutateWorkspaceMembersActivity(workspaceSlug);
} catch (err: unknown) {
const error = err as { error?: string };
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: err?.error || "Something went wrong. Please try again.",
})
);
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(() => {
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);
}
@@ -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,
@@ -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!",
@@ -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 (

View File

@@ -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 {
@@ -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">
@@ -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 "}
@@ -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
@@ -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 ">

View File

@@ -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
@@ -34,7 +35,7 @@ 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
@@ -42,8 +43,8 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
const handleLeaveWorkspace = async () => {
if (!workspaceSlug || !currentUser) return;
await leaveWorkspace(workspaceSlug.toString())
.then(async () => {
try {
await leaveWorkspace(workspaceSlug.toString());
await fetchCurrentUserSettings();
router.push(getWorkspaceRedirectionUrl());
captureSuccess({
@@ -52,33 +53,38 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
workspace: workspaceSlug,
},
});
})
.catch((err: any) => {
} 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: err,
error: errorForCapture,
});
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 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) => {
@@ -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[]
}
keyExtractor={(rowData) => rowData?.member.id ?? ""}
tHeadClassName="border-b border-subtle"
thClassName="text-left font-medium divide-x-0 text-placeholder"

View File

@@ -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";
@@ -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");
@@ -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);
@@ -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);

View File

@@ -45,13 +45,14 @@ export interface IWorkspaceRootStore {
data: Array<{ key: string; is_pinned: boolean; sort_order: number }>
) => Promise<void>;
getNavigationPreferences: (workspaceSlug: string) => IWorkspaceSidebarNavigation | undefined;
mutateWorkspaceMembersActivity: (workspaceSlug: string) => Promise<void>;
// sub-stores
webhook: IWebhookStore;
apiToken: IApiTokenStore;
home: IHomeStore;
}
export class WorkspaceRootStore implements IWorkspaceRootStore {
export abstract class BaseWorkspaceRootStore implements IWorkspaceRootStore {
loader: boolean = false;
// observables
workspaces: Record<string, IWorkspace> = {};
@@ -205,7 +206,7 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
* @param {string} workspaceSlug
* @param {string} logoURL
*/
updateWorkspaceLogo = async (workspaceSlug: string, logoURL: string) => {
updateWorkspaceLogo = (workspaceSlug: string, logoURL: string) => {
const workspaceId = this.getWorkspaceBySlug(workspaceSlug)?.id;
if (!workspaceId) {
throw new Error("Workspace not found");
@@ -219,15 +220,19 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
* delete workspace using the workspace slug
* @param workspaceSlug
*/
deleteWorkspace = async (workspaceSlug: string) =>
await this.workspaceService.deleteWorkspace(workspaceSlug).then(() => {
deleteWorkspace = async (workspaceSlug: string) => {
try {
await this.workspaceService.deleteWorkspace(workspaceSlug);
const updatedWorkspacesList = this.workspaces;
const workspaceId = this.getWorkspaceBySlug(workspaceSlug)?.id;
delete updatedWorkspacesList[`${workspaceId}`];
runInAction(() => {
this.workspaces = updatedWorkspacesList;
});
});
} catch (error) {
console.error("Failed to delete workspace:", error);
}
};
fetchSidebarNavigationPreferences = async (workspaceSlug: string) => {
try {
@@ -309,4 +314,10 @@ export class WorkspaceRootStore implements IWorkspaceRootStore {
throw error;
}
};
/**
* Mutate workspace members activity
* @param workspaceSlug
*/
abstract mutateWorkspaceMembersActivity(workspaceSlug: string): Promise<void>;
}