From cbae761f0811050a41fe85edc2618fc7cfaefcc0 Mon Sep 17 00:00:00 2001 From: Neil Date: Mon, 9 Nov 2026 07:55:37 -0800 Subject: [PATCH] feat(team): team-name rule and name-taken handling to match auth (orca-cloud#697) --- .../orca-profile-org-members-handlers.ts | 4 ++-- .../profile-cloud-dev-org-members.ts | 3 ++- .../profile-cloud-org-members-service.ts | 3 +++ .../billing/team/TeamCreateView.tsx | 20 ++++++++++++++----- .../src/components/billing/team/team-copy.ts | 7 ++++++- src/renderer/src/i18n/locales/en.json | 5 +++-- src/shared/orca-org-management.test.ts | 15 ++++++++++++++ src/shared/orca-org-management.ts | 8 ++++++++ 8 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 src/shared/orca-org-management.test.ts diff --git a/src/main/ipc/profiles/orca-profile-org-members-handlers.ts b/src/main/ipc/profiles/orca-profile-org-members-handlers.ts index 66dab1748ce..29e6a8ef904 100644 --- a/src/main/ipc/profiles/orca-profile-org-members-handlers.ts +++ b/src/main/ipc/profiles/orca-profile-org-members-handlers.ts @@ -1,4 +1,4 @@ -import { ORCA_ORG_NAME_MAX_LENGTH } from '../../../shared/orca-org-management' +import { isValidOrgName, ORCA_ORG_NAME_MAX_LENGTH } from '../../../shared/orca-org-management' import type { OrcaProfileOrgCreateResult } from '../../../shared/orca-org-management' import { ipcMain } from 'electron' import type { @@ -85,7 +85,7 @@ function orgMemberRemoveArgsFromUnknown(args: unknown): OrcaProfileOrgMemberRemo function orgCreateNameFromUnknown(args: unknown): string { const record = args && typeof args === 'object' ? (args as Record) : {} const name = typeof record.name === 'string' ? record.name.trim() : '' - if (!name || name.length > ORCA_ORG_NAME_MAX_LENGTH) { + if (!name || name.length > ORCA_ORG_NAME_MAX_LENGTH || !isValidOrgName(name)) { throw new Error('invalid_orca_org_name') } return name diff --git a/src/main/orca-profiles/profile-cloud-dev-org-members.ts b/src/main/orca-profiles/profile-cloud-dev-org-members.ts index e5a72444869..877032a6f53 100644 --- a/src/main/orca-profiles/profile-cloud-dev-org-members.ts +++ b/src/main/orca-profiles/profile-cloud-dev-org-members.ts @@ -1,3 +1,4 @@ +import { isValidOrgName } from '../../shared/orca-org-management' import type { OrcaProfileOrgCreateResult } from '../../shared/orca-org-management' import { randomUUID } from 'node:crypto' import { addDevOrganization, removeDevOrganization } from './profile-cloud-dev-auth' @@ -160,7 +161,7 @@ export function removeDevOrcaCloudOrgMember( export function createDevOrcaCloudOrg(name: string): OrcaProfileOrgCreateResult { const trimmed = name.trim() - if (!trimmed) { + if (!isValidOrgName(trimmed)) { return { status: 'invalid' } } const orgId = `dev-team-${randomUUID().slice(0, 8)}` diff --git a/src/main/orca-profiles/profile-cloud-org-members-service.ts b/src/main/orca-profiles/profile-cloud-org-members-service.ts index c06408e93a1..34769527669 100644 --- a/src/main/orca-profiles/profile-cloud-org-members-service.ts +++ b/src/main/orca-profiles/profile-cloud-org-members-service.ts @@ -285,6 +285,9 @@ export async function createOrcaProfileOrg( if (result.error.statusCode === 429) { return { status: 'rate-limited' } } + if (result.error.statusCode === 409) { + return { status: 'name-taken' } + } return result.error.statusCode === 400 ? { status: 'invalid' } : { status: 'failed', error: result.error.message } diff --git a/src/renderer/src/components/billing/team/TeamCreateView.tsx b/src/renderer/src/components/billing/team/TeamCreateView.tsx index da54718870e..f90867ee730 100644 --- a/src/renderer/src/components/billing/team/TeamCreateView.tsx +++ b/src/renderer/src/components/billing/team/TeamCreateView.tsx @@ -1,4 +1,4 @@ -import { ORCA_ORG_NAME_MAX_LENGTH } from '../../../../../shared/orca-org-management' +import { isValidOrgName, ORCA_ORG_NAME_MAX_LENGTH } from '../../../../../shared/orca-org-management' import { useId, useState } from 'react' import { ArrowRight, Users } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -27,6 +27,8 @@ export function TeamCreateView(props: TeamCreateViewProps): React.JSX.Element { const id = useId() const [name, setName] = useState('') const trimmed = name.trim() + const nameValid = isValidOrgName(trimmed) + const showRule = trimmed.length > 0 && !nameValid return (
{ event.preventDefault() - if (trimmed && !props.creating && !props.unavailable) { + if (nameValid && !props.creating && !props.unavailable) { props.onCreate(trimmed) } }} @@ -70,8 +72,8 @@ export function TeamCreateView(props: TeamCreateViewProps): React.JSX.Element { 'auto.components.billing.team.teamNamePlaceholder', 'Acme Engineering' )} - aria-invalid={props.error ? true : undefined} - aria-describedby={props.error ? `${id}-error` : undefined} + aria-invalid={props.error || showRule ? true : undefined} + aria-describedby={props.error ? `${id}-error` : showRule ? `${id}-rule` : undefined} onChange={(event) => setName(event.target.value)} readOnly={props.creating} aria-disabled={props.creating || undefined} @@ -79,7 +81,7 @@ export function TeamCreateView(props: TeamCreateViewProps): React.JSX.Element { />