mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-08 16:21:09 -06:00
fix: bulk member invite and table layout (#5209)
This commit is contained in:
@@ -26,12 +26,15 @@ export const EditMemberships = async ({
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-1">{t("common.full_name")}</div>
|
||||
<div className="col-span-1 text-center">{t("common.email")}</div>
|
||||
{canDoRoleManagement && <div className="col-span-1 text-center">{t("common.role")}</div>}
|
||||
<div className="col-span-1 text-center">{t("common.status")}</div>
|
||||
<div className="col-span-1"></div>
|
||||
<div className="flex h-12 w-full max-w-full items-center gap-x-4 rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="w-1/2 overflow-hidden">{t("common.full_name")}</div>
|
||||
<div className="w-1/2 overflow-hidden">{t("common.email")}</div>
|
||||
|
||||
{canDoRoleManagement && <div className="min-w-[100px] whitespace-nowrap">{t("common.role")}</div>}
|
||||
|
||||
<div className="min-w-[80px] whitespace-nowrap">{t("common.status")}</div>
|
||||
|
||||
<div className="min-w-[125px] whitespace-nowrap">{t("common.actions")}</div>
|
||||
</div>
|
||||
|
||||
{role && (
|
||||
|
||||
@@ -110,47 +110,46 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{showDeleteButton && (
|
||||
<>
|
||||
<TooltipRenderer tooltipContent={t("common.delete")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="deleteMemberButton"
|
||||
onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
</>
|
||||
)}
|
||||
<TooltipRenderer tooltipContent={t("common.delete")} shouldRender={!!showDeleteButton}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
id="deleteMemberButton"
|
||||
disabled={!showDeleteButton}
|
||||
onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
{invite && (
|
||||
<>
|
||||
<TooltipRenderer tooltipContent={t("environments.settings.general.share_invite_link")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="shareInviteButton"
|
||||
onClick={() => {
|
||||
handleShareInvite();
|
||||
}}>
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
<TooltipRenderer
|
||||
tooltipContent={t("environments.settings.general.share_invite_link")}
|
||||
shouldRender={!!invite}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="shareInviteButton"
|
||||
disabled={!invite}
|
||||
onClick={() => {
|
||||
handleShareInvite();
|
||||
}}>
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
<TooltipRenderer tooltipContent={t("environments.settings.general.resend_invitation_email")}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="resendInviteButton"
|
||||
onClick={() => {
|
||||
handleResendInvite();
|
||||
}}>
|
||||
<SendHorizonalIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
</>
|
||||
)}
|
||||
<TooltipRenderer
|
||||
tooltipContent={t("environments.settings.general.resend_invitation_email")}
|
||||
shouldRender={!!invite}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
id="resendInviteButton"
|
||||
disabled={!invite}
|
||||
onClick={() => {
|
||||
handleResendInvite();
|
||||
}}>
|
||||
<SendHorizonalIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
<DeleteDialog
|
||||
open={isDeleteMemberModalOpen}
|
||||
|
||||
@@ -86,20 +86,21 @@ export const MembersInfo = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid-cols-5" id="membersInfoWrapper">
|
||||
<div className="max-w-full space-y-4 px-4 py-3" id="membersInfoWrapper">
|
||||
{allMembers.map((member) => (
|
||||
<div
|
||||
className="singleMemberInfo grid h-auto w-full grid-cols-5 content-center rounded-lg px-4 py-3 text-left text-sm text-slate-900"
|
||||
id="singleMemberInfo"
|
||||
className="flex w-full max-w-full items-center gap-x-4 text-left text-sm text-slate-900"
|
||||
key={member.email}>
|
||||
<div className="ph-no-capture col-span-1 flex flex-col justify-center break-all">
|
||||
<p>{member.name}</p>
|
||||
<div className="ph-no-capture w-1/2 overflow-hidden">
|
||||
<p className="w-full truncate">{member.name}</p>
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-1 flex flex-col justify-center break-all text-center">
|
||||
{member.email}
|
||||
<div className="ph-no-capture w-1/2 overflow-hidden">
|
||||
<p className="w-full truncate"> {member.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="ph-no-capture col-span-1 flex flex-col items-center justify-center break-all">
|
||||
{canDoRoleManagement && allMembers?.length > 0 && (
|
||||
{canDoRoleManagement && allMembers?.length > 0 && (
|
||||
<div className="ph-no-capture min-w-[100px]">
|
||||
<EditMembershipRole
|
||||
currentUserRole={currentUserRole}
|
||||
memberRole={member.role}
|
||||
@@ -111,17 +112,16 @@ export const MembersInfo = ({
|
||||
doesOrgHaveMoreThanOneOwner={doesOrgHaveMoreThanOneOwner}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-center">{getMembershipBadge(member)}</div>
|
||||
<div className="col-span-1 flex items-center justify-end gap-x-4 pr-4">
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={!isInvitee(member) ? member : undefined}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-[80px]">{getMembershipBadge(member)}</div>
|
||||
|
||||
<MemberActions
|
||||
organization={organization}
|
||||
member={!isInvitee(member) ? member : undefined}
|
||||
invite={isInvitee(member) ? member : undefined}
|
||||
showDeleteButton={showDeleteButton(member)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -39,19 +39,23 @@ export const BulkInviteTab = ({
|
||||
}
|
||||
Papa.parse(csvFile, {
|
||||
skipEmptyLines: true,
|
||||
comments: "Full Name,Email Address,Role",
|
||||
complete: (results: ParseResult<string[]>) => {
|
||||
header: true,
|
||||
transformHeader: (header) => {
|
||||
if (header === "Full Name") return "name";
|
||||
if (header === "Email Address") return "email";
|
||||
if (header === "Role") return "role";
|
||||
return header;
|
||||
},
|
||||
complete: (results: ParseResult<{ name: string; email: string; role: string }>) => {
|
||||
const members = results.data.map((csv) => {
|
||||
const [name, email, role] = csv;
|
||||
|
||||
let orgRole = canDoRoleManagement ? role.trim().toLowerCase() : "owner";
|
||||
let orgRole = canDoRoleManagement ? csv.role.trim().toLowerCase() : "owner";
|
||||
if (!isFormbricksCloud) {
|
||||
orgRole = orgRole === "billing" ? "owner" : orgRole;
|
||||
}
|
||||
|
||||
return {
|
||||
name: name.trim(),
|
||||
email: email.trim(),
|
||||
name: csv.name.trim(),
|
||||
email: csv.email.trim(),
|
||||
role: orgRole as TOrganizationRole,
|
||||
teamIds: [],
|
||||
};
|
||||
@@ -60,7 +64,6 @@ export const BulkInviteTab = ({
|
||||
ZInvitees.parse(members);
|
||||
onSubmit(members);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
toast.error(t("environments.settings.general.please_check_csv_file"));
|
||||
}
|
||||
setOpen(false);
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe("Invite, accept and remove organization member", async () => {
|
||||
await test.step("Copy invite Link", async () => {
|
||||
await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
const lastMemberInfo = page.locator("#membersInfoWrapper > #singleMemberInfo:last-child");
|
||||
await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
const pendingSpan = lastMemberInfo.locator("span").locator("span").filter({ hasText: "Pending" });
|
||||
@@ -106,7 +106,7 @@ test.describe("Invite, accept and remove organization member", async () => {
|
||||
|
||||
// await expect(page.locator("#membersInfoWrapper")).toBeVisible();
|
||||
|
||||
// const lastMemberInfo = page.locator("#membersInfoWrapper > .singleMemberInfo:last-child");
|
||||
// const lastMemberInfo = page.locator("#membersInfoWrapper > #singleMemberInfo:last-child");
|
||||
// await expect(lastMemberInfo).toBeVisible();
|
||||
|
||||
// const deleteMemberButton = lastMemberInfo.locator("#deleteMemberButton");
|
||||
|
||||
Reference in New Issue
Block a user