diff --git a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx index 0e678408c24..a32572d9127 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(settings)/settings/(workspace)/members/page.tsx @@ -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); diff --git a/apps/web/ce/store/workspace/index.ts b/apps/web/ce/store/workspace/index.ts new file mode 100644 index 00000000000..086063cb4d2 --- /dev/null +++ b/apps/web/ce/store/workspace/index.ts @@ -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 + }; +} diff --git a/apps/web/core/components/workspace/settings/invitations-list-item.tsx b/apps/web/core/components/workspace/settings/invitations-list-item.tsx index ac705dc47ef..9e538b89627 100644 --- a/apps/web/core/components/workspace/settings/invitations-list-item.tsx +++ b/apps/web/core/components/workspace/settings/invitations-list-item.tsx @@ -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 ( diff --git a/apps/web/core/components/workspace/settings/member-columns.tsx b/apps/web/core/components/workspace/settings/member-columns.tsx index 4bac045b830..d7b58432618 100644 --- a/apps/web/core/components/workspace/settings/member-columns.tsx +++ b/apps/web/core/components/workspace/settings/member-columns.tsx @@ -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 ( - {({}) => ( + {() => (
@@ -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={() => (
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} > {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 } }) => ( { + 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={
diff --git a/apps/web/core/components/workspace/settings/members-list-item.tsx b/apps/web/core/components/workspace/settings/members-list-item.tsx index 37bb71eb80c..f980e4eee31 100644 --- a/apps/web/core/components/workspace/settings/members-list-item.tsx +++ b/apps/web/core/components/workspace/settings/members-list-item.tsx @@ -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)} /> )} - 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" diff --git a/apps/web/core/store/root.store.ts b/apps/web/core/store/root.store.ts index 0b82f1321b4..db08030a495 100644 --- a/apps/web/core/store/root.store.ts +++ b/apps/web/core/store/root.store.ts @@ -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); diff --git a/apps/web/core/store/workspace/index.ts b/apps/web/core/store/workspace/index.ts index 3dd1e0eb9c1..eea83642f26 100644 --- a/apps/web/core/store/workspace/index.ts +++ b/apps/web/core/store/workspace/index.ts @@ -45,13 +45,14 @@ export interface IWorkspaceRootStore { data: Array<{ key: string; is_pinned: boolean; sort_order: number }> ) => Promise; getNavigationPreferences: (workspaceSlug: string) => IWorkspaceSidebarNavigation | undefined; + mutateWorkspaceMembersActivity: (workspaceSlug: string) => Promise; // 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 = {}; @@ -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; }