chore(frontend): FIx CI in #9496 (#11870)

This commit is contained in:
sp.wack 2025-12-02 19:17:58 +04:00 committed by GitHub
parent 48b014f368
commit 945cc12d4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 592 additions and 126 deletions

View File

@ -52,14 +52,14 @@ describe("UserContextMenu", () => {
renderUserContextMenu({ type: "user", onClose: vi.fn });
screen.getByTestId("org-selector");
screen.getByText("Logout");
screen.getByText("Settings");
screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
expect(screen.queryByText("Invite Team")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Team")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Account")).not.toBeInTheDocument();
expect(screen.queryByText("ORG$INVITE_TEAM")).not.toBeInTheDocument();
expect(screen.queryByText("ORG$MANAGE_TEAM")).not.toBeInTheDocument();
expect(screen.queryByText("ORG$MANAGE_ACCOUNT")).not.toBeInTheDocument();
expect(
screen.queryByText("Create New Organization"),
screen.queryByText("ORG$CREATE_NEW_ORGANIZATION"),
).not.toBeInTheDocument();
});
@ -67,12 +67,12 @@ describe("UserContextMenu", () => {
renderUserContextMenu({ type: "admin", onClose: vi.fn });
screen.getByTestId("org-selector");
screen.getByText("Invite Team");
screen.getByText("Manage Team");
screen.getByText("Manage Account");
screen.getByText("ORG$INVITE_TEAM");
screen.getByText("ORG$MANAGE_TEAM");
screen.getByText("ORG$MANAGE_ACCOUNT");
expect(
screen.queryByText("Create New Organization"),
screen.queryByText("ORG$CREATE_NEW_ORGANIZATION"),
).not.toBeInTheDocument();
});
@ -80,17 +80,17 @@ describe("UserContextMenu", () => {
renderUserContextMenu({ type: "superadmin", onClose: vi.fn });
screen.getByTestId("org-selector");
screen.getByText("Invite Team");
screen.getByText("Manage Team");
screen.getByText("Manage Account");
screen.getByText("Create New Organization");
screen.getByText("ORG$INVITE_TEAM");
screen.getByText("ORG$MANAGE_TEAM");
screen.getByText("ORG$MANAGE_ACCOUNT");
screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
});
it("should call the logout handler when Logout is clicked", async () => {
const logoutSpy = vi.spyOn(AuthService, "logout");
renderUserContextMenu({ type: "user", onClose: vi.fn });
const logoutButton = screen.getByText("Logout");
const logoutButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await userEvent.click(logoutButton);
expect(logoutSpy).toHaveBeenCalledOnce();
@ -99,7 +99,7 @@ describe("UserContextMenu", () => {
it("should navigate to /settings when Settings is clicked", async () => {
renderUserContextMenu({ type: "user", onClose: vi.fn });
const settingsButton = screen.getByText("Settings");
const settingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
await userEvent.click(settingsButton);
expect(navigateMock).toHaveBeenCalledExactlyOnceWith("/settings");
@ -108,7 +108,7 @@ describe("UserContextMenu", () => {
it("should navigate to /settings/team when Manage Team is clicked", async () => {
renderUserContextMenu({ type: "admin", onClose: vi.fn });
const manageTeamButton = screen.getByText("Manage Team");
const manageTeamButton = screen.getByText("ORG$MANAGE_TEAM");
await userEvent.click(manageTeamButton);
expect(navigateMock).toHaveBeenCalledExactlyOnceWith("/settings/team");
@ -117,7 +117,7 @@ describe("UserContextMenu", () => {
it("should navigate to /settings/org when Manage Account is clicked", async () => {
renderUserContextMenu({ type: "admin", onClose: vi.fn });
const manageTeamButton = screen.getByText("Manage Account");
const manageTeamButton = screen.getByText("ORG$MANAGE_ACCOUNT");
await userEvent.click(manageTeamButton);
expect(navigateMock).toHaveBeenCalledExactlyOnceWith("/settings/org");
@ -129,7 +129,7 @@ describe("UserContextMenu", () => {
expect(screen.queryByTestId("create-org-modal")).not.toBeInTheDocument();
const createOrgButton = screen.getByText("Create New Organization");
const createOrgButton = screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
await userEvent.click(createOrgButton);
const rootOutlet = screen.getByTestId("portal-root");
@ -141,13 +141,13 @@ describe("UserContextMenu", () => {
it("should close the modal when the close button is clicked", async () => {
renderUserContextMenu({ type: "superadmin", onClose: vi.fn });
const createOrgButton = screen.getByText("Create New Organization");
const createOrgButton = screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
await userEvent.click(createOrgButton);
expect(screen.getByTestId("create-org-modal")).toBeInTheDocument();
// Simulate closing the modal
const skipButton = screen.getByRole("button", { name: /skip/i });
const skipButton = screen.getByText("ORG$SKIP");
await userEvent.click(skipButton);
expect(screen.queryByTestId("create-org-modal")).not.toBeInTheDocument();
@ -157,7 +157,7 @@ describe("UserContextMenu", () => {
const createOrgSpy = vi.spyOn(organizationService, "createOrganization");
renderUserContextMenu({ type: "superadmin", onClose: vi.fn });
const createOrgButton = screen.getByText("Create New Organization");
const createOrgButton = screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
await userEvent.click(createOrgButton);
expect(screen.getByTestId("create-org-modal")).toBeInTheDocument();
@ -165,7 +165,7 @@ describe("UserContextMenu", () => {
const orgNameInput = screen.getByTestId("org-name-input");
await userEvent.type(orgNameInput, "New Organization");
const nextButton = screen.getByRole("button", { name: /next/i });
const nextButton = screen.getByText("ORG$NEXT");
await userEvent.click(nextButton);
expect(createOrgSpy).toHaveBeenCalledExactlyOnceWith({
@ -178,13 +178,13 @@ describe("UserContextMenu", () => {
const createOrgSpy = vi.spyOn(organizationService, "createOrganization");
renderUserContextMenu({ type: "superadmin", onClose: vi.fn });
const createOrgButton = screen.getByText("Create New Organization");
const createOrgButton = screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
await userEvent.click(createOrgButton);
const orgNameInput = screen.getByTestId("org-name-input");
await userEvent.type(orgNameInput, "New Organization");
const nextButton = screen.getByRole("button", { name: /next/i });
const nextButton = screen.getByText("ORG$NEXT");
await userEvent.click(nextButton);
expect(createOrgSpy).toHaveBeenCalledExactlyOnceWith({
@ -203,13 +203,13 @@ describe("UserContextMenu", () => {
// Verify invite modal is not visible initially
expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
const createOrgButton = screen.getByText("Create New Organization");
const createOrgButton = screen.getByText("ORG$CREATE_NEW_ORGANIZATION");
await userEvent.click(createOrgButton);
const orgNameInput = screen.getByTestId("org-name-input");
await userEvent.type(orgNameInput, "New Organization");
const nextButton = screen.getByRole("button", { name: /next/i });
const nextButton = screen.getByText("ORG$NEXT");
await userEvent.click(nextButton);
expect(createOrgSpy).toHaveBeenCalledExactlyOnceWith({
@ -246,19 +246,19 @@ describe("UserContextMenu", () => {
const onCloseMock = vi.fn();
renderUserContextMenu({ type: "superadmin", onClose: onCloseMock });
const logoutButton = screen.getByText("Logout");
const logoutButton = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
await userEvent.click(logoutButton);
expect(onCloseMock).toHaveBeenCalledTimes(1);
const settingsButton = screen.getByText("Settings");
const settingsButton = screen.getByText("ACCOUNT_SETTINGS$SETTINGS");
await userEvent.click(settingsButton);
expect(onCloseMock).toHaveBeenCalledTimes(2);
const manageTeamButton = screen.getByText("Manage Team");
const manageTeamButton = screen.getByText("ORG$MANAGE_TEAM");
await userEvent.click(manageTeamButton);
expect(onCloseMock).toHaveBeenCalledTimes(3);
const manageAccountButton = screen.getByText("Manage Account");
const manageAccountButton = screen.getByText("ORG$MANAGE_ACCOUNT");
await userEvent.click(manageAccountButton);
expect(onCloseMock).toHaveBeenCalledTimes(4);
});
@ -271,15 +271,13 @@ describe("UserContextMenu", () => {
const onCloseMock = vi.fn();
renderUserContextMenu({ type: "admin", onClose: onCloseMock });
const inviteButton = screen.getByText("Invite Team");
const inviteButton = screen.getByText("ORG$INVITE_TEAM");
await userEvent.click(inviteButton);
const portalRoot = screen.getByTestId("portal-root");
expect(within(portalRoot).getByTestId("invite-modal")).toBeInTheDocument();
await userEvent.click(
within(portalRoot).getByRole("button", { name: /skip/i }),
);
await userEvent.click(within(portalRoot).getByText("ORG$SKIP"));
expect(inviteMembersBatchSpy).not.toHaveBeenCalled();
});

View File

@ -293,11 +293,13 @@ describe("UserActions", () => {
const userAvatar = screen.getByTestId("user-avatar");
await userEvent.click(userAvatar);
expect(screen.getByTestId("user-context-menu")).toHaveTextContent("Logout");
expect(screen.getByTestId("user-context-menu")).toHaveTextContent(
"Settings",
"ACCOUNT_SETTINGS$LOGOUT",
);
expect(screen.queryByText("Manage Team")).not.toBeInTheDocument();
expect(screen.queryByText("Manage Account")).not.toBeInTheDocument();
expect(screen.getByTestId("user-context-menu")).toHaveTextContent(
"ACCOUNT_SETTINGS$SETTINGS",
);
expect(screen.queryByText("ORG$MANAGE_TEAM")).not.toBeInTheDocument();
expect(screen.queryByText("ORG$MANAGE_ACCOUNT")).not.toBeInTheDocument();
});
});

View File

@ -26,6 +26,7 @@ const RouteStub = createRoutesStub([
path: "/",
},
{
// @ts-expect-error - type mismatch
loader: clientLoader,
Component: SettingsScreen,
path: "/settings",
@ -106,6 +107,8 @@ describe("Manage Org Route", () => {
renderManageOrg();
await screen.findByTestId("manage-org-screen");
await selectOrganization({ orgIndex: 0 }); // user is superadmin in org 1
expect(screen.queryByTestId("add-credits-form")).not.toBeInTheDocument();
// Simulate adding credits
const addCreditsButton = screen.getByText(/add/i);
@ -138,6 +141,8 @@ describe("Manage Org Route", () => {
renderManageOrg();
await screen.findByTestId("manage-org-screen");
await selectOrganization({ orgIndex: 0 }); // user is superadmin in org 1
expect(screen.queryByTestId("add-credits-form")).not.toBeInTheDocument();
// Simulate adding credits
const addCreditsButton = screen.getByText(/add/i);
@ -251,7 +256,7 @@ describe("Manage Org Route", () => {
await selectOrganization({ orgIndex: 2 }); // user is admin in org 3
const deleteOrgButton = screen.queryByRole("button", {
name: /delete organization/i,
name: /ORG\$DELETE_ORGANIZATION/i,
});
expect(deleteOrgButton).not.toBeInTheDocument();
});
@ -269,13 +274,13 @@ describe("Manage Org Route", () => {
).not.toBeInTheDocument();
const deleteOrgButton = screen.getByRole("button", {
name: /delete organization/i,
name: /ORG\$DELETE_ORGANIZATION/i,
});
await userEvent.click(deleteOrgButton);
const deleteConfirmation = screen.getByTestId("delete-org-confirmation");
const confirmButton = within(deleteConfirmation).getByRole("button", {
name: /confirm/i,
name: /BUTTON\$CONFIRM/i,
});
await userEvent.click(confirmButton);

View File

@ -178,7 +178,7 @@ describe("Manage Team Route", () => {
await screen.findByTestId("manage-team-settings");
const inviteButton = screen.queryByRole("button", {
name: /invite team/i,
name: /ORG\$INVITE_TEAM/i,
});
expect(inviteButton).not.toBeInTheDocument();
});
@ -302,7 +302,7 @@ describe("Manage Team Route", () => {
await selectOrganization({ orgIndex: 0 });
const inviteButton = await screen.findByRole("button", {
name: /invite team/i,
name: /ORG\$INVITE_TEAM/i,
});
expect(inviteButton).toBeInTheDocument();
});
@ -313,7 +313,7 @@ describe("Manage Team Route", () => {
expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();
const inviteButton = await screen.findByRole("button", {
name: /invite team/i,
name: /ORG\$INVITE_TEAM/i,
});
await userEvent.click(inviteButton);
@ -329,14 +329,12 @@ describe("Manage Team Route", () => {
await selectOrganization({ orgIndex: 0 });
const inviteButton = await screen.findByRole("button", {
name: /invite team/i,
name: /ORG\$INVITE_TEAM/i,
});
await userEvent.click(inviteButton);
const modal = screen.getByTestId("invite-modal");
const closeButton = within(modal).getByRole("button", {
name: /skip/i,
});
const closeButton = within(modal).getByText("ORG$SKIP");
await userEvent.click(closeButton);
expect(screen.queryByTestId("invite-modal")).not.toBeInTheDocument();

View File

@ -66,7 +66,6 @@ export const organizationService = {
return data;
},
updateMemberRole: async ({
orgId,
userId,

View File

@ -1,6 +1,8 @@
import { useTranslation } from "react-i18next";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { useCreateOrganization } from "#/hooks/mutation/use-create-organization";
import { useSelectedOrganizationId } from "#/context/use-selected-organization";
import { I18nKey } from "#/i18n/declaration";
interface CreateNewOrganizationModalProps {
onClose: () => void;
@ -11,6 +13,7 @@ export function CreateNewOrganizationModal({
onClose,
onSuccess,
}: CreateNewOrganizationModalProps) {
const { t } = useTranslation();
const { mutate: createOrganization } = useCreateOrganization();
const { setOrgId } = useSelectedOrganizationId();
@ -25,7 +28,7 @@ export function CreateNewOrganizationModal({
onClose();
onSuccess?.();
},
}
},
);
}
};
@ -39,13 +42,13 @@ export function CreateNewOrganizationModal({
>
<form action={formAction}>
<label>
Organization Name
{t(I18nKey.ORG$ORGANIZATION_NAME)}
<input data-testid="org-name-input" name="org-name" type="text" />
</label>
<button type="submit">Next</button>
<button type="submit">{t(I18nKey.ORG$NEXT)}</button>
<button type="button" onClick={onClose}>
Skip
{t(I18nKey.ORG$SKIP)}
</button>
</form>
</div>

View File

@ -1,8 +1,10 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { useInviteMembersBatch } from "#/hooks/mutation/use-invite-members-batch";
import { BrandButton } from "../settings/brand-button";
import { BadgeInput } from "#/components/shared/inputs/badge-input";
import { I18nKey } from "#/i18n/declaration";
interface InviteOrganizationMemberModalProps {
onClose: (event?: React.MouseEvent<HTMLButtonElement>) => void;
@ -11,6 +13,7 @@ interface InviteOrganizationMemberModalProps {
export function InviteOrganizationMemberModal({
onClose,
}: InviteOrganizationMemberModalProps) {
const { t } = useTranslation();
const { mutate: inviteMembers } = useInviteMembersBatch();
const [emails, setEmails] = React.useState<string[]>([]);
@ -29,12 +32,14 @@ export function InviteOrganizationMemberModal({
onClick={(e) => e.stopPropagation()}
>
<div className="w-full flex flex-col gap-2">
<h3 className="text-lg font-semibold">Invite Users</h3>
<h3 className="text-lg font-semibold">
{t(I18nKey.ORG$INVITE_USERS)}
</h3>
<p className="text-xs text-gray-400">
Invite colleaguess using their email address
{t(I18nKey.ORG$INVITE_USERS_DESCRIPTION)}
</p>
<div className="flex flex-col gap-2">
<span className="text-sm">Emails</span>
<span className="text-sm">{t(I18nKey.ORG$EMAILS)}</span>
<BadgeInput
name="emails-badge-input"
value={emails}
@ -50,7 +55,7 @@ export function InviteOrganizationMemberModal({
className="flex-1"
onClick={formAction}
>
Next
{t(I18nKey.ORG$NEXT)}
</BrandButton>
<BrandButton
type="button"
@ -58,7 +63,7 @@ export function InviteOrganizationMemberModal({
onClick={onClose}
className="flex-1"
>
Skip
{t(I18nKey.ORG$SKIP)}
</BrandButton>
</div>
</div>

View File

@ -1,7 +1,9 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ChevronDown } from "lucide-react";
import { OrganizationMember, OrganizationUserRole } from "#/types/org";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
interface OrganizationMemberListItemProps {
email: OrganizationMember["email"];
@ -21,6 +23,7 @@ export function OrganizationMemberListItem({
onRoleChange,
onRemove,
}: OrganizationMemberListItemProps) {
const { t } = useTranslation();
const [roleSelectionOpen, setRoleSelectionOpen] = React.useState(false);
const handleRoleSelectionClick = (newRole: OrganizationUserRole) => {
@ -44,7 +47,7 @@ export function OrganizationMemberListItem({
</span>
{status === "invited" && (
<span className="text-xs text-tertiary-light border border-tertiary px-2 py-1 rounded-lg">
invited
{t(I18nKey.ORG$STATUS_INVITED)}
</span>
)}
</div>
@ -62,20 +65,24 @@ export function OrganizationMemberListItem({
{roleSelectionIsPermitted && roleSelectionOpen && (
<ul data-testid="role-dropdown">
<li>
<span onClick={() => handleRoleSelectionClick("admin")}>admin</span>
<span onClick={() => handleRoleSelectionClick("admin")}>
{t(I18nKey.ORG$ROLE_ADMIN)}
</span>
</li>
<li>
<span onClick={() => handleRoleSelectionClick("user")}>user</span>
<span onClick={() => handleRoleSelectionClick("user")}>
{t(I18nKey.ORG$ROLE_USER)}
</span>
</li>
<li>
<span
className="text-red-500 cursor-pointer"
<span
className="text-red-500 cursor-pointer"
onClick={() => {
onRemove?.();
setRoleSelectionOpen(false);
}}
>
remove
{t(I18nKey.ORG$REMOVE)}
</span>
</li>
</ul>

View File

@ -41,7 +41,7 @@ export function SetupPaymentModal() {
variant="primary"
className="w-full"
isDisabled={isPending}
onClick={mutate}
onClick={() => mutate()}
>
{t(I18nKey.BILLING$PROCEED_TO_STRIPE)}
</BrandButton>

View File

@ -67,7 +67,7 @@ export function UserActions({ user, isLoading }: UserActionsProps) {
/>
</div>
)}
{accountContextMenuIsVisible && !!user && (
{accountContextMenuIsVisible && !!user && shouldShowUserActions && (
<div className="w-sm absolute left-[calc(100%+12px)] bottom-0 z-10">
<UserContextMenu
type={me?.role || "user"}

View File

@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import {
IoCardOutline,
IoLogOutOutline,
@ -18,6 +19,7 @@ import { InviteOrganizationMemberModal } from "../org/invite-organization-member
import { useSelectedOrganizationId } from "#/context/use-selected-organization";
import { useOrganizations } from "#/hooks/query/use-organizations";
import { SettingsDropdownInput } from "../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
interface TempButtonProps {
start: React.ReactNode;
@ -51,6 +53,7 @@ interface UserContextMenuProps {
}
export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const { orgId, setOrgId } = useSelectedOrganizationId();
const { data: organizations } = useOrganizations();
@ -117,7 +120,9 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
document.getElementById("portal-root") || document.body,
)}
<h3 className="text-lg font-semibold text-white">Account</h3>
<h3 className="text-lg font-semibold text-white">
{t(I18nKey.ORG$ACCOUNT)}
</h3>
<div className="flex flex-col items-start gap-2">
<SettingsDropdownInput
@ -130,7 +135,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
...(organizations?.map((org) => ({
key: org.id,
label: org.name,
})) || [])
})) || []),
]}
onSelectionChange={(org) => {
if (org === "personal") {
@ -149,7 +154,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
onClick={handleInviteMemberClick}
start={<IoPersonAddOutline className="text-white" size={14} />}
>
Invite Team
{t(I18nKey.ORG$INVITE_TEAM)}
</TempButton>
<TempDivider />
@ -158,13 +163,13 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
onClick={handleManageAccountClick}
start={<IoCardOutline className="text-white" size={14} />}
>
Manage Account
{t(I18nKey.ORG$MANAGE_ACCOUNT)}
</TempButton>
<TempButton
onClick={handleManageTeamClick}
start={<IoPersonOutline className="text-white" size={14} />}
>
Manage Team
{t(I18nKey.ORG$MANAGE_TEAM)}
</TempButton>
</>
)}
@ -175,7 +180,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
onClick={handleSettingsClick}
start={<FaCog className="text-white" size={14} />}
>
Settings
{t(I18nKey.ACCOUNT_SETTINGS$SETTINGS)}
</TempButton>
{isSuperAdmin && (
@ -183,7 +188,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
onClick={handleCreateNewOrgClick}
start={<FaPlus className="text-white" size={14} />}
>
Create New Organization
{t(I18nKey.ORG$CREATE_NEW_ORGANIZATION)}
</TempButton>
)}
@ -191,7 +196,7 @@ export function UserContextMenu({ type, onClose }: UserContextMenuProps) {
onClick={handleLogout}
start={<IoLogOutOutline className="text-white" size={14} />}
>
Logout
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
</TempButton>
</div>
</div>

View File

@ -118,7 +118,6 @@ const renderUserMessageWithSkillReady = (
);
} catch (error) {
// If skill ready event creation fails, just render the user message
console.error("Failed to create skill ready event:", error);
return (
<UserAssistantEventMessage
event={messageEvent}

View File

@ -15,4 +15,4 @@ export const useInviteMembersBatch = () => {
});
},
});
};
};

View File

@ -15,4 +15,4 @@ export const useRemoveMember = () => {
});
},
});
};
};

View File

@ -955,4 +955,28 @@ export enum I18nKey {
COMMON$PLAN_AGENT_DESCRIPTION = "COMMON$PLAN_AGENT_DESCRIPTION",
PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED = "PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED",
OBSERVATION_MESSAGE$SKILL_READY = "OBSERVATION_MESSAGE$SKILL_READY",
ORG$ORGANIZATION_NAME = "ORG$ORGANIZATION_NAME",
ORG$NEXT = "ORG$NEXT",
ORG$SKIP = "ORG$SKIP",
ORG$INVITE_USERS = "ORG$INVITE_USERS",
ORG$INVITE_USERS_DESCRIPTION = "ORG$INVITE_USERS_DESCRIPTION",
ORG$EMAILS = "ORG$EMAILS",
ORG$STATUS_INVITED = "ORG$STATUS_INVITED",
ORG$ROLE_ADMIN = "ORG$ROLE_ADMIN",
ORG$ROLE_USER = "ORG$ROLE_USER",
ORG$REMOVE = "ORG$REMOVE",
ORG$ACCOUNT = "ORG$ACCOUNT",
ORG$INVITE_TEAM = "ORG$INVITE_TEAM",
ORG$MANAGE_ACCOUNT = "ORG$MANAGE_ACCOUNT",
ORG$MANAGE_TEAM = "ORG$MANAGE_TEAM",
ORG$CREATE_NEW_ORGANIZATION = "ORG$CREATE_NEW_ORGANIZATION",
ORG$CHANGE_ORG_NAME = "ORG$CHANGE_ORG_NAME",
ORG$MODIFY_ORG_NAME_DESCRIPTION = "ORG$MODIFY_ORG_NAME_DESCRIPTION",
ORG$ADD_CREDITS = "ORG$ADD_CREDITS",
ORG$CREDITS = "ORG$CREDITS",
ORG$ADD = "ORG$ADD",
ORG$BILLING_INFORMATION = "ORG$BILLING_INFORMATION",
ORG$CHANGE = "ORG$CHANGE",
ORG$DELETE_ORGANIZATION = "ORG$DELETE_ORGANIZATION",
ACCOUNT_SETTINGS$SETTINGS = "ACCOUNT_SETTINGS$SETTINGS",
}

View File

@ -15278,5 +15278,389 @@
"tr": "Yetenek hazır",
"de": "Fähigkeit bereit",
"uk": "Навичка готова"
},
"ORG$ORGANIZATION_NAME": {
"en": "Organization Name",
"ja": "組織名",
"zh-CN": "组织名称",
"zh-TW": "組織名稱",
"ko-KR": "조직 이름",
"no": "Organisasjonsnavn",
"it": "Nome organizzazione",
"pt": "Nome da organização",
"es": "Nombre de la organización",
"ar": "اسم المنظمة",
"fr": "Nom de l'organisation",
"tr": "Organizasyon Adı",
"de": "Organisationsname",
"uk": "Назва організації"
},
"ORG$NEXT": {
"en": "Next",
"ja": "次へ",
"zh-CN": "下一步",
"zh-TW": "下一步",
"ko-KR": "다음",
"no": "Neste",
"it": "Avanti",
"pt": "Próximo",
"es": "Siguiente",
"ar": "التالي",
"fr": "Suivant",
"tr": "İleri",
"de": "Weiter",
"uk": "Далі"
},
"ORG$SKIP": {
"en": "Skip",
"ja": "スキップ",
"zh-CN": "跳过",
"zh-TW": "跳過",
"ko-KR": "건너뛰기",
"no": "Hopp over",
"it": "Salta",
"pt": "Pular",
"es": "Omitir",
"ar": "تخطي",
"fr": "Passer",
"tr": "Atla",
"de": "Überspringen",
"uk": "Пропустити"
},
"ORG$INVITE_USERS": {
"en": "Invite Users",
"ja": "ユーザーを招待",
"zh-CN": "邀请用户",
"zh-TW": "邀請用戶",
"ko-KR": "사용자 초대",
"no": "Inviter brukere",
"it": "Invita utenti",
"pt": "Convidar usuários",
"es": "Invitar usuarios",
"ar": "دعوة المستخدمين",
"fr": "Inviter des utilisateurs",
"tr": "Kullanıcı Davet Et",
"de": "Benutzer einladen",
"uk": "Запросити користувачів"
},
"ORG$INVITE_USERS_DESCRIPTION": {
"en": "Invite colleagues using their email address",
"ja": "メールアドレスを使用して同僚を招待",
"zh-CN": "使用电子邮件地址邀请同事",
"zh-TW": "使用電子郵件地址邀請同事",
"ko-KR": "이메일 주소로 동료 초대",
"no": "Inviter kolleger med e-postadresse",
"it": "Invita colleghi usando il loro indirizzo email",
"pt": "Convide colegas usando o endereço de email",
"es": "Invita a colegas usando su dirección de correo",
"ar": "دعوة الزملاء باستخدام عنوان بريدهم الإلكتروني",
"fr": "Invitez des collègues en utilisant leur adresse email",
"tr": "E-posta adresi kullanarak meslektaşlarını davet et",
"de": "Laden Sie Kollegen per E-Mail-Adresse ein",
"uk": "Запросіть колег за їхньою електронною адресою"
},
"ORG$EMAILS": {
"en": "Emails",
"ja": "メール",
"zh-CN": "电子邮件",
"zh-TW": "電子郵件",
"ko-KR": "이메일",
"no": "E-poster",
"it": "Email",
"pt": "E-mails",
"es": "Correos electrónicos",
"ar": "رسائل البريد الإلكتروني",
"fr": "E-mails",
"tr": "E-postalar",
"de": "E-Mails",
"uk": "Електронні листи"
},
"ORG$STATUS_INVITED": {
"en": "invited",
"ja": "招待済み",
"zh-CN": "已邀请",
"zh-TW": "已邀請",
"ko-KR": "초대됨",
"no": "invitert",
"it": "invitato",
"pt": "convidado",
"es": "invitado",
"ar": "تمت الدعوة",
"fr": "invité",
"tr": "davet edildi",
"de": "eingeladen",
"uk": "запрошений"
},
"ORG$ROLE_ADMIN": {
"en": "admin",
"ja": "管理者",
"zh-CN": "管理员",
"zh-TW": "管理員",
"ko-KR": "관리자",
"no": "admin",
"it": "admin",
"pt": "admin",
"es": "admin",
"ar": "مدير",
"fr": "admin",
"tr": "yönetici",
"de": "Admin",
"uk": "адміністратор"
},
"ORG$ROLE_USER": {
"en": "user",
"ja": "ユーザー",
"zh-CN": "用户",
"zh-TW": "用戶",
"ko-KR": "사용자",
"no": "bruker",
"it": "utente",
"pt": "usuário",
"es": "usuario",
"ar": "مستخدم",
"fr": "utilisateur",
"tr": "kullanıcı",
"de": "Benutzer",
"uk": "користувач"
},
"ORG$REMOVE": {
"en": "remove",
"ja": "削除",
"zh-CN": "移除",
"zh-TW": "移除",
"ko-KR": "제거",
"no": "fjern",
"it": "rimuovi",
"pt": "remover",
"es": "eliminar",
"ar": "إزالة",
"fr": "supprimer",
"tr": "kaldır",
"de": "entfernen",
"uk": "видалити"
},
"ORG$ACCOUNT": {
"en": "Account",
"ja": "アカウント",
"zh-CN": "账户",
"zh-TW": "帳戶",
"ko-KR": "계정",
"no": "Konto",
"it": "Account",
"pt": "Conta",
"es": "Cuenta",
"ar": "الحساب",
"fr": "Compte",
"tr": "Hesap",
"de": "Konto",
"uk": "Обліковий запис"
},
"ORG$INVITE_TEAM": {
"en": "Invite Team",
"ja": "チームを招待",
"zh-CN": "邀请团队",
"zh-TW": "邀請團隊",
"ko-KR": "팀 초대",
"no": "Inviter team",
"it": "Invita team",
"pt": "Convidar equipe",
"es": "Invitar equipo",
"ar": "دعوة الفريق",
"fr": "Inviter l'équipe",
"tr": "Takım Davet Et",
"de": "Team einladen",
"uk": "Запросити команду"
},
"ORG$MANAGE_ACCOUNT": {
"en": "Manage Account",
"ja": "アカウント管理",
"zh-CN": "管理账户",
"zh-TW": "管理帳戶",
"ko-KR": "계정 관리",
"no": "Administrer konto",
"it": "Gestisci account",
"pt": "Gerenciar conta",
"es": "Administrar cuenta",
"ar": "إدارة الحساب",
"fr": "Gérer le compte",
"tr": "Hesabı Yönet",
"de": "Konto verwalten",
"uk": "Керувати обліковим записом"
},
"ORG$MANAGE_TEAM": {
"en": "Manage Team",
"ja": "チーム管理",
"zh-CN": "管理团队",
"zh-TW": "管理團隊",
"ko-KR": "팀 관리",
"no": "Administrer team",
"it": "Gestisci team",
"pt": "Gerenciar equipe",
"es": "Administrar equipo",
"ar": "إدارة الفريق",
"fr": "Gérer l'équipe",
"tr": "Takımı Yönet",
"de": "Team verwalten",
"uk": "Керувати командою"
},
"ORG$CREATE_NEW_ORGANIZATION": {
"en": "Create New Organization",
"ja": "新しい組織を作成",
"zh-CN": "创建新组织",
"zh-TW": "建立新組織",
"ko-KR": "새 조직 만들기",
"no": "Opprett ny organisasjon",
"it": "Crea nuova organizzazione",
"pt": "Criar nova organização",
"es": "Crear nueva organización",
"ar": "إنشاء منظمة جديدة",
"fr": "Créer une nouvelle organisation",
"tr": "Yeni Organizasyon Oluştur",
"de": "Neue Organisation erstellen",
"uk": "Створити нову організацію"
},
"ORG$CHANGE_ORG_NAME": {
"en": "Change Org Name",
"ja": "組織名を変更",
"zh-CN": "更改组织名称",
"zh-TW": "更改組織名稱",
"ko-KR": "조직 이름 변경",
"no": "Endre organisasjonsnavn",
"it": "Cambia nome organizzazione",
"pt": "Alterar nome da organização",
"es": "Cambiar nombre de organización",
"ar": "تغيير اسم المنظمة",
"fr": "Changer le nom de l'organisation",
"tr": "Organizasyon Adını Değiştir",
"de": "Organisationsname ändern",
"uk": "Змінити назву організації"
},
"ORG$MODIFY_ORG_NAME_DESCRIPTION": {
"en": "Modify your Org Name and Save",
"ja": "組織名を変更して保存",
"zh-CN": "修改您的组织名称并保存",
"zh-TW": "修改您的組織名稱並儲存",
"ko-KR": "조직 이름을 수정하고 저장",
"no": "Endre organisasjonsnavnet og lagre",
"it": "Modifica il nome dell'organizzazione e salva",
"pt": "Modifique o nome da organização e salve",
"es": "Modifica el nombre de la organización y guarda",
"ar": "قم بتعديل اسم المنظمة واحفظ",
"fr": "Modifiez le nom de l'organisation et enregistrez",
"tr": "Organizasyon adını değiştir ve kaydet",
"de": "Organisationsname ändern und speichern",
"uk": "Змініть назву організації та збережіть"
},
"ORG$ADD_CREDITS": {
"en": "Add Credits",
"ja": "クレジットを追加",
"zh-CN": "添加积分",
"zh-TW": "新增點數",
"ko-KR": "크레딧 추가",
"no": "Legg til kreditter",
"it": "Aggiungi crediti",
"pt": "Adicionar créditos",
"es": "Añadir créditos",
"ar": "إضافة رصيد",
"fr": "Ajouter des crédits",
"tr": "Kredi Ekle",
"de": "Credits hinzufügen",
"uk": "Додати кредити"
},
"ORG$CREDITS": {
"en": "Credits",
"ja": "クレジット",
"zh-CN": "积分",
"zh-TW": "點數",
"ko-KR": "크레딧",
"no": "Kreditter",
"it": "Crediti",
"pt": "Créditos",
"es": "Créditos",
"ar": "الرصيد",
"fr": "Crédits",
"tr": "Krediler",
"de": "Credits",
"uk": "Кредити"
},
"ORG$ADD": {
"en": "+ Add",
"ja": "+ 追加",
"zh-CN": "+ 添加",
"zh-TW": "+ 新增",
"ko-KR": "+ 추가",
"no": "+ Legg til",
"it": "+ Aggiungi",
"pt": "+ Adicionar",
"es": "+ Añadir",
"ar": "+ إضافة",
"fr": "+ Ajouter",
"tr": "+ Ekle",
"de": "+ Hinzufügen",
"uk": "+ Додати"
},
"ORG$BILLING_INFORMATION": {
"en": "Billing Information",
"ja": "請求情報",
"zh-CN": "账单信息",
"zh-TW": "帳單資訊",
"ko-KR": "결제 정보",
"no": "Faktureringsinformasjon",
"it": "Informazioni di fatturazione",
"pt": "Informações de cobrança",
"es": "Información de facturación",
"ar": "معلومات الفوترة",
"fr": "Informations de facturation",
"tr": "Fatura Bilgisi",
"de": "Rechnungsinformationen",
"uk": "Платіжна інформація"
},
"ORG$CHANGE": {
"en": "Change",
"ja": "変更",
"zh-CN": "更改",
"zh-TW": "變更",
"ko-KR": "변경",
"no": "Endre",
"it": "Modifica",
"pt": "Alterar",
"es": "Cambiar",
"ar": "تغيير",
"fr": "Modifier",
"tr": "Değiştir",
"de": "Ändern",
"uk": "Змінити"
},
"ORG$DELETE_ORGANIZATION": {
"en": "Delete Organization",
"ja": "組織を削除",
"zh-CN": "删除组织",
"zh-TW": "刪除組織",
"ko-KR": "조직 삭제",
"no": "Slett organisasjon",
"it": "Elimina organizzazione",
"pt": "Excluir organização",
"es": "Eliminar organización",
"ar": "حذف المنظمة",
"fr": "Supprimer l'organisation",
"tr": "Organizasyonu Sil",
"de": "Organisation löschen",
"uk": "Видалити організацію"
},
"ACCOUNT_SETTINGS$SETTINGS": {
"en": "Settings",
"ja": "設定",
"zh-CN": "设置",
"zh-TW": "設定",
"ko-KR": "설정",
"no": "Innstillinger",
"it": "Impostazioni",
"pt": "Configurações",
"es": "Configuración",
"ar": "الإعدادات",
"fr": "Paramètres",
"tr": "Ayarlar",
"de": "Einstellungen",
"uk": "Налаштування"
}
}

View File

@ -258,7 +258,6 @@ export const ORG_HANDLERS = [
);
}),
http.patch(
"/api/organizations/:orgId/members",
async ({ request, params }) => {
@ -315,31 +314,37 @@ export const ORG_HANDLERS = [
return HttpResponse.json({ message: "Member removed" }, { status: 200 });
}),
http.post("/api/organizations/:orgId/invite/batch", async ({ request, params }) => {
const { emails } = (await request.json()) as { emails: string[] };
const orgId = params.orgId?.toString();
http.post(
"/api/organizations/:orgId/invite/batch",
async ({ request, params }) => {
const { emails } = (await request.json()) as { emails: string[] };
const orgId = params.orgId?.toString();
if (!emails || emails.length === 0) {
return HttpResponse.json({ error: "Emails are required" }, { status: 400 });
}
if (!emails || emails.length === 0) {
return HttpResponse.json(
{ error: "Emails are required" },
{ status: 400 },
);
}
if (!orgId || !ORGS_AND_MEMBERS[orgId]) {
return HttpResponse.json(
{ error: "Organization not found" },
{ status: 404 },
);
}
if (!orgId || !ORGS_AND_MEMBERS[orgId]) {
return HttpResponse.json(
{ error: "Organization not found" },
{ status: 404 },
);
}
const members = Array.from(ORGS_AND_MEMBERS[orgId]);
const newMembers = emails.map((email, index) => ({
id: String(members.length + index + 1),
email,
role: "user" as const,
status: "invited" as const,
}));
const members = Array.from(ORGS_AND_MEMBERS[orgId]);
const newMembers = emails.map((email, index) => ({
id: String(members.length + index + 1),
email,
role: "user" as const,
status: "invited" as const,
}));
ORGS_AND_MEMBERS[orgId] = [...members, ...newMembers];
ORGS_AND_MEMBERS[orgId] = [...members, ...newMembers];
return HttpResponse.json(newMembers, { status: 201 });
}),
return HttpResponse.json(newMembers, { status: 201 });
},
),
];

View File

@ -1,6 +1,7 @@
import React from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { redirect, useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { useCreateStripeCheckoutSession } from "#/hooks/mutation/stripe/use-create-stripe-checkout-session";
import { useOrganization } from "#/hooks/query/use-organization";
import { useOrganizationPaymentInfo } from "#/hooks/query/use-organization-payment-info";
@ -17,6 +18,7 @@ import {
getMeFromQueryClient,
} from "#/utils/query-client-getters";
import { queryClient } from "#/query-client-config";
import { I18nKey } from "#/i18n/declaration";
function TempChip({
children,
@ -70,7 +72,7 @@ function TempButton({
variant === "primary" && "bg-[#F3CE49] text-black",
variant === "secondary" && "bg-[#737373] text-white",
)}
type={type}
type={type === "submit" ? "submit" : "button"}
onClick={onClick}
>
{children}
@ -83,12 +85,15 @@ interface ChangeOrgNameModalProps {
}
function ChangeOrgNameModal({ onClose }: ChangeOrgNameModalProps) {
const { t } = useTranslation();
const { orgId } = useSelectedOrganizationId();
const queryClient = useQueryClient();
const qClient = useQueryClient();
const { mutate: updateOrganization } = useMutation({
mutationFn: (name: string) =>
organizationService.updateOrganization({ orgId, name }),
mutationFn: (name: string) => {
if (!orgId) throw new Error("Organization ID is required");
return organizationService.updateOrganization({ orgId, name });
},
});
const formAction = (formData: FormData) => {
@ -97,7 +102,7 @@ function ChangeOrgNameModal({ onClose }: ChangeOrgNameModalProps) {
if (orgName?.trim()) {
updateOrganization(orgName, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["organizations", orgId] });
qClient.invalidateQueries({ queryKey: ["organizations", orgId] });
onClose();
},
});
@ -115,19 +120,24 @@ function ChangeOrgNameModal({ onClose }: ChangeOrgNameModalProps) {
)}
>
<div className="flex flex-col gap-2 w-full">
<h3 className="text-lg font-semibold">Change Org Name</h3>
<p className="text-xs text-gray-400">Modify your Org Name and Save</p>
<h3 className="text-lg font-semibold">
{t(I18nKey.ORG$CHANGE_ORG_NAME)}
</h3>
<p className="text-xs text-gray-400">
{t(I18nKey.ORG$MODIFY_ORG_NAME_DESCRIPTION)}
</p>
<SettingsInput
name="org-name"
type="text"
required
className="w-full"
label="Organization Name"
placeholder="Enter new organization name"
/>
</div>
<BrandButton variant="primary" type="submit" className="w-full">
Save
{t(I18nKey.BUTTON$SAVE)}
</BrandButton>
</form>
</ModalBackdrop>
@ -141,13 +151,17 @@ interface DeleteOrgConfirmationModalProps {
function DeleteOrgConfirmationModal({
onClose,
}: DeleteOrgConfirmationModalProps) {
const queryClient = useQueryClient();
const { t } = useTranslation();
const qClient = useQueryClient();
const navigate = useNavigate();
const { orgId, setOrgId } = useSelectedOrganizationId();
const { mutate: deleteOrganization } = useMutation({
mutationFn: () => organizationService.deleteOrganization({ orgId }),
mutationFn: () => {
if (!orgId) throw new Error("Organization ID is required");
return organizationService.deleteOrganization({ orgId });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["organizations"] });
qClient.invalidateQueries({ queryKey: ["organizations"] });
setOrgId(null);
navigate("/");
},
@ -163,7 +177,7 @@ function DeleteOrgConfirmationModal({
})
}
>
Confirm
{t(I18nKey.BUTTON$CONFIRM)}
</button>
</div>
);
@ -174,6 +188,7 @@ interface AddCreditsModalProps {
}
function AddCreditsModal({ onClose }: AddCreditsModalProps) {
const { t } = useTranslation();
const { mutate: addBalance } = useCreateStripeCheckoutSession();
const formAction = (formData: FormData) => {
@ -193,7 +208,9 @@ function AddCreditsModal({ onClose }: AddCreditsModalProps) {
className="w-sm rounded-xl bg-[#171717] flex flex-col p-6 gap-6"
>
<div className="flex flex-col gap-2">
<h3 className="text-xl font-semibold">Add Credits</h3>
<h3 className="text-xl font-semibold">
{t(I18nKey.ORG$ADD_CREDITS)}
</h3>
<input
data-testid="amount-input"
name="amount"
@ -203,9 +220,9 @@ function AddCreditsModal({ onClose }: AddCreditsModalProps) {
</div>
<div className="flex gap-2">
<TempButton type="submit">Next</TempButton>
<TempButton type="submit">{t(I18nKey.ORG$NEXT)}</TempButton>
<TempButton type="button" onClick={onClose} variant="secondary">
Cancel
{t(I18nKey.BUTTON$CANCEL)}
</TempButton>
</div>
</form>
@ -231,6 +248,7 @@ export const clientLoader = async () => {
};
function ManageOrg() {
const { t } = useTranslation();
const { data: me } = useMe();
const { data: organization } = useOrganization();
const { data: organizationPaymentInfo } = useOrganizationPaymentInfo();
@ -246,6 +264,8 @@ function ManageOrg() {
!!me && rolePermissions[me.role].includes("change_organization_name");
const canDeleteOrg =
!!me && rolePermissions[me.role].includes("delete_organization");
const canAddCredits =
!!me && rolePermissions[me.role].includes("add_credits");
return (
<div
@ -264,14 +284,18 @@ function ManageOrg() {
)}
<div className="flex flex-col gap-2">
<span className="text-white text-xs font-semibold ml-1">Credits</span>
<span className="text-white text-xs font-semibold ml-1">
{t(I18nKey.ORG$CREDITS)}
</span>
<div className="flex items-center gap-2">
<TempChip data-testid="available-credits">
{organization?.balance}
</TempChip>
<TempInteractiveChip onClick={() => setAddCreditsFormVisible(true)}>
+ Add
</TempInteractiveChip>
{canAddCredits && (
<TempInteractiveChip onClick={() => setAddCreditsFormVisible(true)}>
{t(I18nKey.ORG$ADD)}
</TempInteractiveChip>
)}
</div>
</div>
@ -281,7 +305,7 @@ function ManageOrg() {
<div data-testid="org-name" className="flex flex-col gap-2 w-sm">
<span className="text-white text-xs font-semibold ml-1">
Organization Name
{t(I18nKey.ORG$ORGANIZATION_NAME)}
</span>
<div
@ -297,7 +321,7 @@ function ManageOrg() {
onClick={() => setChangeOrgNameFormVisible(true)}
className="text-[#A3A3A3] hover:text-white transition-colors cursor-pointer"
>
Change
{t(I18nKey.ORG$CHANGE)}
</button>
)}
</div>
@ -305,7 +329,7 @@ function ManageOrg() {
<div className="flex flex-col gap-2 w-sm">
<span className="text-white text-xs font-semibold ml-1">
Billing Information
{t(I18nKey.ORG$BILLING_INFORMATION)}
</span>
<span
@ -325,7 +349,7 @@ function ManageOrg() {
onClick={() => setDeleteOrgConfirmationVisible(true)}
className="text-xs text-[#FF3B30] cursor-pointer font-semibold hover:underline"
>
Delete Organization
{t(I18nKey.ORG$DELETE_ORGANIZATION)}
</button>
)}
</div>

View File

@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import { useTranslation } from "react-i18next";
import { Plus } from "lucide-react";
import { redirect } from "react-router";
import { InviteOrganizationMemberModal } from "#/components/features/org/invite-organization-member-modal";
@ -17,6 +18,7 @@ import {
getSelectedOrgFromQueryClient,
getMeFromQueryClient,
} from "#/utils/query-client-getters";
import { I18nKey } from "#/i18n/declaration";
export const clientLoader = async () => {
const selectedOrgId = getSelectedOrgFromQueryClient();
@ -36,6 +38,7 @@ export const clientLoader = async () => {
};
function ManageTeam() {
const { t } = useTranslation();
const { data: organizationMembers } = useOrganizationMembers();
const { data: user } = useMe();
const { mutate: updateMemberRole } = useUpdateMemberRole();
@ -82,7 +85,7 @@ function ManageTeam() {
className="flex items-center gap-1"
>
<Plus size={14} />
Invite Team
{t(I18nKey.ORG$INVITE_TEAM)}
</BrandButton>
)}

View File

@ -8,17 +8,20 @@ type ChangeUserRolePermission =
type ChangeOrganizationNamePermission = "change_organization_name";
type DeleteOrganizationPermission = "delete_organization";
type AddCreditsPermission = "add_credits";
type UserPermission =
| InviteUserToOrganizationKey
| ChangeUserRolePermission
| ChangeOrganizationNamePermission
| DeleteOrganizationPermission;
| DeleteOrganizationPermission
| AddCreditsPermission;
const superadminPerms: UserPermission[] = [
"invite_user_to_organization",
"change_organization_name",
"delete_organization",
"add_credits",
"change_user_role:superadmin",
"change_user_role:admin",
"change_user_role:user",

View File

@ -10,7 +10,9 @@ window.scrollTo = vi.fn();
// Mock ResizeObserver for test environment
class MockResizeObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}