mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 11:57:56 +01:00
chore: settings workspace members enchancements
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
18
apps/web/ce/store/workspace/index.ts
Normal file
18
apps/web/ce/store/workspace/index.ts
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -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,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);
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 ">
|
||||
|
||||
@@ -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,43 +43,48 @@ export const WorkspaceMembersListItem = observer(function WorkspaceMembersListIt
|
||||
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) => {
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user