diff --git a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx
index 936eda0ba18..74b45236c9a 100644
--- a/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx
+++ b/web/app/[workspaceSlug]/(projects)/settings/members/page.tsx
@@ -12,14 +12,17 @@ import { IWorkspaceBulkInviteFormData } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components
import { NotAuthorizedView } from "@/components/auth-screens";
+import { CountChip } from "@/components/common";
import { PageHead } from "@/components/core";
-import { SendWorkspaceInvitationModal, WorkspaceMembersList } from "@/components/workspace";
-// constants
+import { WorkspaceMembersList } from "@/components/workspace";
// helpers
import { cn } from "@/helpers/common.helper";
import { getUserRole } from "@/helpers/user.helper";
// hooks
import { useEventTracker, useMember, useUserPermissions, useWorkspace } from "@/hooks/store";
+// plane web components
+import { BillingActionsButton } from "@/plane-web/components/workspace/billing";
+import { SendWorkspaceInvitationModal } from "@/plane-web/components/workspace/members";
const WorkspaceMembersSettingsPage = observer(() => {
// states
@@ -31,7 +34,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
const { captureEvent } = useEventTracker();
const {
- workspace: { inviteMembersToWorkspace },
+ workspace: { workspaceMemberIds, inviteMembersToWorkspace },
} = useMember();
const { currentWorkspace } = useWorkspace();
const { t } = useTranslation();
@@ -83,6 +86,7 @@ const WorkspaceMembersSettingsPage = observer(() => {
title: "Error!",
message: `${err.error ?? t("something_went_wrong_please_try_again")}`,
});
+ throw err;
});
};
@@ -107,8 +111,13 @@ const WorkspaceMembersSettingsPage = observer(() => {
"opacity-60": !canPerformWorkspaceMemberActions,
})}
>
-
-
{t("workspace_settings.settings.members.title")}
+
+
+ {t("workspace_settings.settings.members.title")}
+ {workspaceMemberIds && workspaceMemberIds.length > 0 && (
+
+ )}
+
{
{t("workspace_settings.settings.members.add_member")}
)}
+
diff --git a/web/ce/components/workspace/billing/billing-actions-button.tsx b/web/ce/components/workspace/billing/billing-actions-button.tsx
new file mode 100644
index 00000000000..b15d67b9111
--- /dev/null
+++ b/web/ce/components/workspace/billing/billing-actions-button.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import { observer } from "mobx-react";
+
+export type TBillingActionsButtonProps = {
+ canPerformWorkspaceAdminActions: boolean;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const BillingActionsButton = observer((props: TBillingActionsButtonProps) => <>>);
diff --git a/web/ce/components/workspace/billing/index.ts b/web/ce/components/workspace/billing/index.ts
index 1efe34c51ec..f02c34d3f6d 100644
--- a/web/ce/components/workspace/billing/index.ts
+++ b/web/ce/components/workspace/billing/index.ts
@@ -1 +1,2 @@
export * from "./root";
+export * from "./billing-actions-button";
diff --git a/web/ce/components/workspace/index.ts b/web/ce/components/workspace/index.ts
index 94a16694754..ab13bb3907e 100644
--- a/web/ce/components/workspace/index.ts
+++ b/web/ce/components/workspace/index.ts
@@ -2,3 +2,4 @@ export * from "./edition-badge";
export * from "./upgrade-badge";
export * from "./billing";
export * from "./delete-workspace-section";
+export * from "./members";
diff --git a/web/ce/components/workspace/members/index.ts b/web/ce/components/workspace/members/index.ts
new file mode 100644
index 00000000000..5e1651e901c
--- /dev/null
+++ b/web/ce/components/workspace/members/index.ts
@@ -0,0 +1 @@
+export * from "./invite-modal";
diff --git a/web/ce/components/workspace/members/invite-modal.tsx b/web/ce/components/workspace/members/invite-modal.tsx
new file mode 100644
index 00000000000..629370cc14c
--- /dev/null
+++ b/web/ce/components/workspace/members/invite-modal.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import React from "react";
+import { observer } from "mobx-react";
+import { useParams } from "next/navigation";
+// plane imports
+import { useTranslation } from "@plane/i18n";
+import { IWorkspaceBulkInviteFormData } from "@plane/types";
+// ui
+import { EModalWidth, EModalPosition, ModalCore } from "@plane/ui";
+// components
+import { InvitationFields, InvitationModalActions } from "@/components/workspace/invite-modal";
+import { InvitationForm } from "@/components/workspace/invite-modal/form";
+// hooks
+import { useWorkspaceInvitationActions } from "@/hooks/use-workspace-invitation";
+
+export type TSendWorkspaceInvitationModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onSubmit: (data: IWorkspaceBulkInviteFormData) => Promise
| undefined;
+};
+
+export const SendWorkspaceInvitationModal: React.FC = observer((props) => {
+ const { isOpen, onClose, onSubmit } = props;
+ // store hooks
+ const { t } = useTranslation();
+ // router
+ const { workspaceSlug } = useParams();
+ // derived values
+ const { control, fields, formState, remove, onFormSubmit, handleClose, appendField } = useWorkspaceInvitationActions({
+ onSubmit,
+ onClose,
+ });
+
+ return (
+
+
+ }
+ className="p-5"
+ >
+
+
+
+ );
+});
diff --git a/web/core/components/workspace/index.ts b/web/core/components/workspace/index.ts
index a8f404d1b96..29c7a050497 100644
--- a/web/core/components/workspace/index.ts
+++ b/web/core/components/workspace/index.ts
@@ -5,4 +5,4 @@ export * from "./confirm-workspace-member-remove";
export * from "./create-workspace-form";
export * from "./delete-workspace-modal";
export * from "./logo";
-export * from "./send-workspace-invitation-modal";
+export * from "./invite-modal";
diff --git a/web/core/components/workspace/invite-modal/actions.tsx b/web/core/components/workspace/invite-modal/actions.tsx
new file mode 100644
index 00000000000..e6c535282b4
--- /dev/null
+++ b/web/core/components/workspace/invite-modal/actions.tsx
@@ -0,0 +1,64 @@
+import { observer } from "mobx-react";
+import { Plus } from "lucide-react";
+// plane imports
+import { useTranslation } from "@plane/i18n";
+import { Button } from "@plane/ui";
+import { cn } from "@plane/utils";
+
+type TInvitationModalActionsProps = {
+ isInviteDisabled?: boolean;
+ isSubmitting?: boolean;
+ handleClose: () => void;
+ appendField: () => void;
+ addMoreButtonText?: string;
+ submitButtonText?: {
+ default: string;
+ loading: string;
+ };
+ cancelButtonText?: string;
+ className?: string;
+};
+
+export const InvitationModalActions: React.FC = observer((props) => {
+ const {
+ isInviteDisabled = false,
+ isSubmitting = false,
+ handleClose,
+ appendField,
+ addMoreButtonText,
+ submitButtonText,
+ cancelButtonText,
+ className,
+ } = props;
+ // store hooks
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+
+
+ );
+});
diff --git a/web/core/components/workspace/invite-modal/fields.tsx b/web/core/components/workspace/invite-modal/fields.tsx
new file mode 100644
index 00000000000..d46a2499542
--- /dev/null
+++ b/web/core/components/workspace/invite-modal/fields.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { observer } from "mobx-react";
+import { Control, Controller, FieldArrayWithId, FormState } from "react-hook-form";
+import { X } from "lucide-react";
+// plane imports
+import { ROLE } from "@plane/constants";
+import { useTranslation } from "@plane/i18n";
+import { CustomSelect, Input } from "@plane/ui";
+import { cn } from "@plane/utils";
+// hooks
+import { useUserPermissions } from "@/hooks/store";
+import { InvitationFormValues } from "@/hooks/use-workspace-invitation";
+
+type TInvitationFieldsProps = {
+ workspaceSlug: string;
+ fields: FieldArrayWithId[];
+ control: Control;
+ formState: FormState;
+ remove: (index: number) => void;
+ className?: string;
+};
+
+export const InvitationFields = observer((props: TInvitationFieldsProps) => {
+ const {
+ workspaceSlug,
+ fields,
+ control,
+ formState: { errors },
+ remove,
+ className,
+ } = props;
+ // plane hooks
+ const { t } = useTranslation();
+ // store hooks
+ const { workspaceInfoBySlug } = useUserPermissions();
+ // derived values
+ const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug.toString())?.role;
+
+ return (
+
+ {fields.map((field, index) => (
+
+
+ (
+ <>
+
+ {errors.emails?.[index]?.email && (
+ {errors.emails?.[index]?.email?.message}
+ )}
+ >
+ )}
+ />
+
+
+
+ (
+ {ROLE[value]}}
+ onChange={onChange}
+ optionsClassName="w-full"
+ className="flex-grow w-24"
+ input
+ >
+ {Object.entries(ROLE).map(([key, value]) => {
+ if (currentWorkspaceRole && currentWorkspaceRole >= parseInt(key))
+ return (
+
+ {value}
+
+ );
+ })}
+
+ )}
+ />
+
+ {fields.length > 1 && (
+
+
+
+ )}
+
+
+ ))}
+
+ );
+});
diff --git a/web/core/components/workspace/invite-modal/form.tsx b/web/core/components/workspace/invite-modal/form.tsx
new file mode 100644
index 00000000000..1178f1f6ecf
--- /dev/null
+++ b/web/core/components/workspace/invite-modal/form.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { observer } from "mobx-react";
+import { Dialog } from "@headlessui/react";
+
+type TInvitationFormProps = {
+ title: string;
+ description: React.ReactNode;
+ children: React.ReactNode;
+ onSubmit: () => void;
+ actions: React.ReactNode;
+ className?: string;
+};
+
+export const InvitationForm = observer((props: TInvitationFormProps) => {
+ const { title, description, children, actions, onSubmit, className } = props;
+
+ return (
+
+ );
+});
diff --git a/web/core/components/workspace/invite-modal/index.ts b/web/core/components/workspace/invite-modal/index.ts
new file mode 100644
index 00000000000..235edf9800b
--- /dev/null
+++ b/web/core/components/workspace/invite-modal/index.ts
@@ -0,0 +1,3 @@
+export * from "./actions";
+export * from "./fields";
+export * from "./form";
diff --git a/web/core/components/workspace/send-workspace-invitation-modal.tsx b/web/core/components/workspace/send-workspace-invitation-modal.tsx
deleted file mode 100644
index a30e8e51fd1..00000000000
--- a/web/core/components/workspace/send-workspace-invitation-modal.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-"use client";
-
-import React, { useEffect } from "react";
-import { observer } from "mobx-react";
-import { useParams } from "next/navigation";
-import { Controller, useFieldArray, useForm } from "react-hook-form";
-import { Plus, X } from "lucide-react";
-import { Dialog, Transition } from "@headlessui/react";
-// plane imports
-import { ROLE, EUserPermissions } from "@plane/constants";
-import { useTranslation } from "@plane/i18n";
-import { IWorkspaceBulkInviteFormData } from "@plane/types";
-// ui
-import { Button, CustomSelect, Input } from "@plane/ui";
-// constants
-// hooks
-import { useUserPermissions } from "@/hooks/store";
-// types
-
-type Props = {
- isOpen: boolean;
- onClose: () => void;
- onSubmit: (data: IWorkspaceBulkInviteFormData) => Promise | undefined;
-};
-
-type EmailRole = {
- email: string;
- role: EUserPermissions;
-};
-
-type FormValues = {
- emails: EmailRole[];
-};
-
-const defaultValues: FormValues = {
- emails: [
- {
- email: "",
- role: 15,
- },
- ],
-};
-
-export const SendWorkspaceInvitationModal: React.FC = observer((props) => {
- const { isOpen, onClose, onSubmit } = props;
- // store hooks
- const { workspaceInfoBySlug } = useUserPermissions();
- const { t } = useTranslation();
- // router
- const { workspaceSlug } = useParams();
- // form info
- const {
- control,
- reset,
- handleSubmit,
- formState: { isSubmitting, errors },
- } = useForm();
-
- const { fields, append, remove } = useFieldArray({
- control,
- name: "emails",
- });
-
- const currentWorkspaceRole = workspaceInfoBySlug(workspaceSlug.toString())?.role;
-
- const handleClose = () => {
- onClose();
-
- const timeout = setTimeout(() => {
- reset(defaultValues);
- clearTimeout(timeout);
- }, 350);
- };
-
- const appendField = () => {
- append({ email: "", role: 15 });
- };
-
- const onSubmitForm = async (data: FormValues) => {
- await onSubmit(data)?.then(() => {
- reset(defaultValues);
- });
- };
-
- useEffect(() => {
- if (fields.length === 0) append([{ email: "", role: 15 }]);
- }, [fields, append]);
-
- return (
-
-
-
- );
-});
diff --git a/web/core/hooks/use-workspace-invitation.tsx b/web/core/hooks/use-workspace-invitation.tsx
new file mode 100644
index 00000000000..20d19e894f0
--- /dev/null
+++ b/web/core/hooks/use-workspace-invitation.tsx
@@ -0,0 +1,84 @@
+import { useEffect } from "react";
+import { Control, FieldArrayWithId, FormState, useFieldArray, useForm, UseFormWatch } from "react-hook-form";
+// plane imports
+import { EUserPermissions } from "@plane/constants";
+
+type EmailRole = {
+ email: string;
+ role: EUserPermissions;
+};
+
+export type InvitationFormValues = {
+ emails: EmailRole[];
+};
+
+const SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES: InvitationFormValues = {
+ emails: [
+ {
+ email: "",
+ role: EUserPermissions.MEMBER,
+ },
+ ],
+};
+
+type TUseWorkspaceInvitationProps = {
+ onSubmit: (data: InvitationFormValues) => Promise | undefined;
+ onClose: () => void;
+};
+
+type TUseWorkspaceInvitationReturn = {
+ control: Control;
+ fields: FieldArrayWithId[];
+ formState: FormState;
+ watch: UseFormWatch;
+ remove: (index: number) => void;
+ onFormSubmit: () => void;
+ handleClose: () => void;
+ appendField: () => void;
+};
+
+export const useWorkspaceInvitationActions = (props: TUseWorkspaceInvitationProps): TUseWorkspaceInvitationReturn => {
+ const { onSubmit, onClose } = props;
+ // form info
+ const { control, reset, watch, handleSubmit, formState } = useForm({
+ defaultValues: SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES,
+ });
+
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: "emails",
+ });
+
+ const handleClose = () => {
+ onClose();
+ const timeout = setTimeout(() => {
+ reset(SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES);
+ clearTimeout(timeout);
+ }, 350);
+ };
+
+ const appendField = () => {
+ append({ email: "", role: EUserPermissions.MEMBER });
+ };
+
+ const onSubmitForm = async (data: InvitationFormValues) => {
+ await onSubmit(data)?.then(() => {
+ reset(SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES);
+ });
+ };
+
+ useEffect(() => {
+ if (fields.length === 0) append([{ email: "", role: EUserPermissions.MEMBER }]);
+ }, [fields, append]);
+
+ return {
+ control,
+ fields,
+ formState,
+ watch,
+ remove,
+ onFormSubmit: handleSubmit(onSubmitForm),
+ handleClose,
+ appendField,
+ };
+};