mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 13:52:43 +08:00
parent
48b014f368
commit
945cc12d4e
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -66,7 +66,6 @@ export const organizationService = {
|
||||
return data;
|
||||
},
|
||||
|
||||
|
||||
updateMemberRole: async ({
|
||||
orgId,
|
||||
userId,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -15,4 +15,4 @@ export const useInviteMembersBatch = () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -15,4 +15,4 @@ export const useRemoveMember = () => {
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -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",
|
||||
}
|
||||
|
||||
@ -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": "Налаштування"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -10,7 +10,9 @@ window.scrollTo = vi.fn();
|
||||
// Mock ResizeObserver for test environment
|
||||
class MockResizeObserver {
|
||||
observe = vi.fn();
|
||||
|
||||
unobserve = vi.fn();
|
||||
|
||||
disconnect = vi.fn();
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user