feat(ui): invite a teammate from the AWS connect step (#12917)

This commit is contained in:
Alejandro Bailo
2026-10-01 09:50:10 +02:00
committed by GitHub
parent f0da33f451
commit 4605d9a770
35 changed files with 1449 additions and 163 deletions
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { toSentInvitation } from "./invitation.adapter";
const created = {
data: {
id: "inv-1",
type: "invitations",
attributes: {
email: "teammate@company.com",
token: "abc123DEF45678",
state: "pending",
expires_at: "2026-10-07T10:00:00Z",
},
},
};
describe("toSentInvitation", () => {
it("reads the id, email and token of a created invitation", () => {
expect(toSentInvitation(created)).toEqual({
id: "inv-1",
email: "teammate@company.com",
token: "abc123DEF45678",
});
});
it("returns null when the action resolved without a value", () => {
// A 5xx makes `sendInvite` resolve undefined.
expect(toSentInvitation(undefined)).toBeNull();
});
it("returns null on a rejection, with or without an errors array", () => {
expect(
toSentInvitation({ errors: [{ detail: "Invalid email" }] }),
).toBeNull();
expect(toSentInvitation({ error: "Something went wrong" })).toBeNull();
});
it("returns null when the record is missing any of the fields the link needs", () => {
expect(
toSentInvitation({
data: { id: "inv-1", attributes: { email: "a@b.com" } },
}),
).toBeNull();
expect(
toSentInvitation({
data: { id: "inv-1", attributes: { token: "abc123DEF45678" } },
}),
).toBeNull();
});
});
@@ -0,0 +1,25 @@
import type { SentInvitation } from "@/types/onboarding-invite";
const readString = (value: unknown): string | null =>
typeof value === "string" && value.length > 0 ? value : null;
/**
* The created record out of `sendInvite`'s JSON:API response. Null for every
* failure shape: `undefined` (a 5xx makes the action resolve without a value),
* `{ errors }`, a bare `{ error }`, or a record missing what the link needs.
*/
export function toSentInvitation(response: unknown): SentInvitation | null {
if (!response || typeof response !== "object") return null;
const { data } = response as { data?: unknown };
if (!data || typeof data !== "object") return null;
const { id, attributes } = data as { id?: unknown; attributes?: unknown };
const fields =
attributes && typeof attributes === "object"
? (attributes as Record<string, unknown>)
: {};
const invitationId = readString(id);
const email = readString(fields.email);
const token = readString(fields.token);
if (!invitationId || !email || !token) return null;
return { id: invitationId, email, token };
}
@@ -5,11 +5,9 @@ import type { InvitationRoleOption } from "@/types/onboarding-invite";
const ROLES_PAGE_SIZE = 50;
// Roles the onboarding invite step can offer; empty when the read fails so
// the step can fall back to skipping rather than blocking the checkpoint.
export const getOnboardingInviteRoles = async (): Promise<
InvitationRoleOption[]
> => {
// Roles an invitation can grant; empty when the read fails so a caller can
// fall back (skip, disable) rather than block.
export const getInvitationRoles = async (): Promise<InvitationRoleOption[]> => {
const rolesData = await getRoles({ pageSize: ROLES_PAGE_SIZE });
const roles: unknown = rolesData?.data;
if (!Array.isArray(roles)) return [];