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 { const {
workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore }, workspace: { workspaceMemberIds, inviteMembersToWorkspace, filtersStore },
} = useMember(); } = useMember();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace, mutateWorkspaceMembersActivity } = useWorkspace();
const { t } = useTranslation(); const { t } = useTranslation();
// derived values // derived values
@@ -55,6 +55,7 @@ const WorkspaceMembersSettingsPage = observer(function WorkspaceMembersSettingsP
const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => { const handleWorkspaceInvite = async (data: IWorkspaceBulkInviteFormData) => {
try { try {
await inviteMembersToWorkspace(workspaceSlug, data); await inviteMembersToWorkspace(workspaceSlug, data);
void mutateWorkspaceMembersActivity(workspaceSlug);
setInviteModal(false); 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 { captureClick } from "@/helpers/event-tracker.helper";
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
type Props = { type Props = {
invitationId: string; invitationId: string;
@@ -31,6 +32,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
const { t } = useTranslation(); const { t } = useTranslation();
// store hooks // store hooks
const { allowPermissions, workspaceInfoBySlug } = useUserPermissions(); const { allowPermissions, workspaceInfoBySlug } = useUserPermissions();
const { mutateWorkspaceMembersActivity } = useWorkspace();
const { const {
workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails }, workspace: { updateMemberInvitation, deleteMemberInvitation, getWorkspaceInvitationDetails },
} = useMember(); } = useMember();
@@ -50,36 +52,36 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
); );
const handleRemoveInvitation = async () => { const handleRemoveInvitation = async () => {
if (!workspaceSlug || !invitationDetails) return; try {
if (!workspaceSlug || !invitationDetails) return;
await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id) await deleteMemberInvitation(workspaceSlug.toString(), invitationDetails.id);
.then(() => { setToast({
setToast({ type: TOAST_TYPE.SUCCESS,
type: TOAST_TYPE.SUCCESS, title: "Success!",
title: "Success!", message: "Invitation removed successfully.",
message: "Invitation removed successfully.", });
}); void mutateWorkspaceMembersActivity(workspaceSlug);
}) } catch (err: unknown) {
.catch((err) => const error = err as { error?: string };
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "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; if (!invitationDetails || !currentWorkspaceMemberInfo) return null;
const handleCopyText = () => { const handleCopyText = async () => {
try { try {
const inviteLink = new URL(invitationDetails.invite_link, window.location.origin).href; const inviteLink = new URL(invitationDetails.invite_link, window.location.origin).href;
copyTextToClipboard(inviteLink).then(() => { await copyTextToClipboard(inviteLink);
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: t("common.link_copied"), title: t("common.link_copied"),
message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }), message: t("entity.link_copied_to_clipboard", { entity: t("common.invite") }),
});
}); });
} catch (error) { } catch (error) {
console.error("Error generating invite link:", error); console.error("Error generating invite link:", error);
@@ -89,7 +91,7 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
const MENU_ITEMS: TContextMenuItem[] = [ const MENU_ITEMS: TContextMenuItem[] = [
{ {
key: "copy-link", key: "copy-link",
action: handleCopyText, action: () => void handleCopyText(),
title: t("common.actions.copy_link"), title: t("common.actions.copy_link"),
icon: LinkIcon, icon: LinkIcon,
shouldRender: !!invitationDetails.invite_link, shouldRender: !!invitationDetails.invite_link,
@@ -157,7 +159,8 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, { updateMemberInvitation(workspaceSlug.toString(), invitationDetails.id, {
role: value, role: value,
}).catch((error) => { }).catch((err: unknown) => {
const error = err as { error?: string };
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
@@ -169,7 +172,11 @@ export const WorkspaceInvitationsListItem = observer(function WorkspaceInvitatio
placement="bottom-end" placement="bottom-end"
> >
{Object.keys(ROLE).map((key) => { {Object.keys(ROLE).map((key) => {
if (currentWorkspaceRole && currentWorkspaceRole !== 20 && currentWorkspaceRole < parseInt(key)) if (
currentWorkspaceRole &&
Number(currentWorkspaceRole) !== 20 &&
Number(currentWorkspaceRole) < parseInt(key)
)
return null; return null;
return ( return (

View File

@@ -16,6 +16,7 @@ import { getFileURL } from "@plane/utils";
// hooks // hooks
import { useMember } from "@/hooks/store/use-member"; import { useMember } from "@/hooks/store/use-member";
import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useUser, useUserPermissions } from "@/hooks/store/user";
import { useWorkspace } from "@/hooks/store/use-workspace";
// plane web constants // plane web constants
export interface RowData { export interface RowData {
@@ -45,7 +46,7 @@ export function NameColumn(props: NameProps) {
return ( return (
<Disclosure> <Disclosure>
{({}) => ( {() => (
<div className="relative group"> <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-4 gap-y-2 w-72 justify-between">
<div className="flex items-center gap-x-2 gap-y-2 flex-1"> <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" 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={() => ( render={() => (
<div <div
role="button"
tabIndex={0}
className="flex items-center gap-x-3 cursor-pointer" className="flex items-center gap-x-3 cursor-pointer"
onClick={() => setRemoveMemberModal(rowData)} 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} data-ph-element={MEMBER_TRACKER_ELEMENTS.WORKSPACE_MEMBER_TABLE_CONTEXT_MENU}
> >
<Trash2 className="size-3.5 align-middle" /> {id === currentUser?.id ? "Leave " : "Remove "} <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 { const {
workspace: { updateMember }, workspace: { updateMember },
} = useMember(); } = useMember();
const { mutateWorkspaceMembersActivity } = useWorkspace();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
// derived values // derived values
@@ -139,22 +149,24 @@ export const AccountTypeColumn = observer(function AccountTypeColumn(props: Acco
rules={{ required: "Role is required." }} rules={{ required: "Role is required." }}
render={({ field: { value } }) => ( render={({ field: { value } }) => (
<CustomSelect <CustomSelect
value={value} value={value as EUserPermissions}
onChange={(value: EUserPermissions) => { onChange={async (value: EUserPermissions) => {
if (!workspaceSlug) return; if (!workspaceSlug) return;
updateMember(workspaceSlug.toString(), rowData.member.id, { try {
role: value as unknown as EUserPermissions, // Cast value to unknown first, then to EUserPermissions await updateMember(workspaceSlug.toString(), rowData.member.id, {
}).catch((err) => { role: value as unknown as EUserPermissions,
console.log(err, "err"); });
const error = err.error; void mutateWorkspaceMembersActivity(workspaceSlug);
const errorString = Array.isArray(error) ? error[0] : error; } catch (err: unknown) {
const error = err as { error?: string | string[] };
const errorString = Array.isArray(error?.error) ? error.error[0] : error?.error;
setToast({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "Error!", title: "Error!",
message: errorString ?? "An error occurred while updating member role. Please try again.", message: errorString ?? "An error occurred while updating member role. Please try again.",
}); });
}); }
}} }}
label={ label={
<div className="flex "> <div className="flex ">

View File

@@ -9,6 +9,7 @@ import { Table } from "@plane/ui";
// components // components
import { MembersLayoutLoader } from "@/components/ui/loader/layouts/members-layout-loader"; import { MembersLayoutLoader } from "@/components/ui/loader/layouts/members-layout-loader";
import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove"; import { ConfirmWorkspaceMemberRemove } from "@/components/workspace/confirm-workspace-member-remove";
import type { RowData } from "@/components/workspace/settings/member-columns";
// helpers // helpers
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper"; import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
// hooks // hooks
@@ -34,7 +35,7 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
workspace: { removeMemberFromWorkspace }, workspace: { removeMemberFromWorkspace },
} = useMember(); } = useMember();
const { leaveWorkspace } = useUserPermissions(); const { leaveWorkspace } = useUserPermissions();
const { getWorkspaceRedirectionUrl } = useWorkspace(); const { getWorkspaceRedirectionUrl, mutateWorkspaceMembersActivity } = useWorkspace();
const { fetchCurrentUserSettings } = useUserSettings(); const { fetchCurrentUserSettings } = useUserSettings();
const { t } = useTranslation(); const { t } = useTranslation();
// derived values // derived values
@@ -42,43 +43,48 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
const handleLeaveWorkspace = async () => { const handleLeaveWorkspace = async () => {
if (!workspaceSlug || !currentUser) return; if (!workspaceSlug || !currentUser) return;
await leaveWorkspace(workspaceSlug.toString()) try {
.then(async () => { await leaveWorkspace(workspaceSlug.toString());
await fetchCurrentUserSettings(); await fetchCurrentUserSettings();
router.push(getWorkspaceRedirectionUrl()); router.push(getWorkspaceRedirectionUrl());
captureSuccess({ captureSuccess({
eventName: MEMBER_TRACKER_EVENTS.workspace.leave, eventName: MEMBER_TRACKER_EVENTS.workspace.leave,
payload: { payload: {
workspace: workspaceSlug, 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"),
});
}); });
} 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) => { const handleRemoveMember = async (memberId: string) => {
if (!workspaceSlug || !memberId) return; 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({ setToast({
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR,
title: "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) => { const handleRemove = async (memberId: string) => {
@@ -109,9 +115,11 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
onSubmit={() => handleRemove(removeMemberModal.member.id)} onSubmit={() => handleRemove(removeMemberModal.member.id)}
/> />
)} )}
<Table <Table<RowData>
columns={columns ?? []} 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 ?? ""} keyExtractor={(rowData) => rowData?.member.id ?? ""}
tHeadClassName="border-b border-subtle" tHeadClassName="border-b border-subtle"
thClassName="text-left font-medium divide-x-0 text-placeholder" 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 { RootStore } from "@/plane-web/store/root.store";
import type { IStateStore } from "@/plane-web/store/state.store"; import type { IStateStore } from "@/plane-web/store/state.store";
import { StateStore } from "@/plane-web/store/state.store"; import { StateStore } from "@/plane-web/store/state.store";
import { WorkspaceRootStore } from "@/plane-web/store/workspace";
// stores // stores
import type { ICycleStore } from "./cycle.store"; import type { ICycleStore } from "./cycle.store";
import { CycleStore } from "./cycle.store"; import { CycleStore } from "./cycle.store";
@@ -61,7 +62,6 @@ import { ThemeStore } from "./theme.store";
import type { IUserStore } from "./user"; import type { IUserStore } from "./user";
import { UserStore } from "./user"; import { UserStore } from "./user";
import type { IWorkspaceRootStore } from "./workspace"; import type { IWorkspaceRootStore } from "./workspace";
import { WorkspaceRootStore } from "./workspace";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@@ -102,7 +102,7 @@ export class CoreRootStore {
this.instance = new InstanceStore(); this.instance = new InstanceStore();
this.user = new UserStore(this as unknown as RootStore); this.user = new UserStore(this as unknown as RootStore);
this.theme = new ThemeStore(); this.theme = new ThemeStore();
this.workspaceRoot = new WorkspaceRootStore(this); this.workspaceRoot = new WorkspaceRootStore(this as unknown as RootStore);
this.projectRoot = new ProjectRootStore(this); this.projectRoot = new ProjectRootStore(this);
this.memberRoot = new MemberRootStore(this as unknown as RootStore); this.memberRoot = new MemberRootStore(this as unknown as RootStore);
this.cycle = new CycleStore(this); this.cycle = new CycleStore(this);
@@ -136,7 +136,7 @@ export class CoreRootStore {
this.commandPalette = new CommandPaletteStore(); this.commandPalette = new CommandPaletteStore();
this.instance = new InstanceStore(); this.instance = new InstanceStore();
this.user = new UserStore(this as unknown as RootStore); 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.projectRoot = new ProjectRootStore(this);
this.memberRoot = new MemberRootStore(this as unknown as RootStore); this.memberRoot = new MemberRootStore(this as unknown as RootStore);
this.cycle = new CycleStore(this); this.cycle = new CycleStore(this);

View File

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