fix: show clear error when creating or updating organization site name to an existing one (#793)

* fixed error of not showing the org name API response error

* Fixed the error also for organization settings page

* modified

* Keep field-specific mapping in OrgApi

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Victor Jack
2026-08-06 06:24:07 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 8ad0141ab8
commit a83e172e6d
7 changed files with 89 additions and 17 deletions
+8 -1
View File
@@ -4,6 +4,7 @@ import type { TGetAudienceQuery, TGetOrganizationCoursesQuery } from '@cio/utils
import type { TNewOrganizationPlan, TOrganization, TOrganizationPlan } from '@db/types'; import type { TNewOrganizationPlan, TOrganization, TOrganizationPlan } from '@db/types';
import { import {
cancelOrganizationPlan, cancelOrganizationPlan,
checkSiteNameExists,
createOrganizationPlan, createOrganizationPlan,
deleteOrganizationAudienceMember, deleteOrganizationAudienceMember,
deleteOrganizationMember, deleteOrganizationMember,
@@ -648,7 +649,13 @@ export async function createOrgPlan(data: TNewOrganizationPlan) {
*/ */
export async function updateOrg(orgId: string, data: Partial<TOrganization>) { export async function updateOrg(orgId: string, data: Partial<TOrganization>) {
try { try {
// Enforce plan restriction: only paid plans can use non-minimal landing page themes if (data.siteName) {
const exists = await checkSiteNameExists(data.siteName, orgId); // exclude current org
if (exists) {
throw new AppError('Site name already exists', ErrorCodes.SITENAME_EXISTS, 409, 'siteName');
}
}
if (data.landingpage && typeof data.landingpage === 'object') { if (data.landingpage && typeof data.landingpage === 'object') {
const landingpage = data.landingpage as Record<string, unknown>; const landingpage = data.landingpage as Record<string, unknown>;
const theme = landingpage.theme; const theme = landingpage.theme;
@@ -55,8 +55,19 @@ export class OnboardingApi extends BaseApiWithErrors {
return; return;
} }
// map api validation back to frontend fields
if ('error' in result) { if ('error' in result) {
// Specific handling for siteName / orgName
if (result.code === 'SITENAME_EXISTS' || result.field === 'siteName') {
this.errors = { ...this.errors, siteName: result.error };
return;
}
if (result.code === 'ORGNAME_EXISTS' || result.field === 'orgName') {
this.errors = { ...this.errors, orgName: result.error };
return;
}
// Fallback to generic
this.handleValidationError(result); this.handleValidationError(result);
} }
} }
@@ -44,6 +44,7 @@ import { snackbar } from '$features/ui/snackbar/store';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { uploadImage } from '$lib/utils/services/upload'; import { uploadImage } from '$lib/utils/services/upload';
import { DEFAULT_ORG_AUDIENCE_QUERY, toAudienceRequestQuery } from '../utils/audience-query-utils'; import { DEFAULT_ORG_AUDIENCE_QUERY, toAudienceRequestQuery } from '../utils/audience-query-utils';
import type { ZodError } from 'zod';
export interface TOrgUpdateForm { export interface TOrgUpdateForm {
name?: string; name?: string;
@@ -309,11 +310,8 @@ class OrgApi extends BaseApiWithErrors {
snackbar.error(result); snackbar.error(result);
return; return;
} }
if ('error' in result && 'field' in result) { if ('error' in result) {
// Field-specific error automatically mapped to this.errors[field] this.handleValidationError(result);
this.errors[result.field as string] = result.error;
} else if ('error' in result) {
this.errors.general = result.error;
} }
} }
}); });
@@ -340,6 +338,11 @@ class OrgApi extends BaseApiWithErrors {
if (!result.success) { if (!result.success) {
this.errors = mapZodErrorsToTranslations(result.error, 'organization'); this.errors = mapZodErrorsToTranslations(result.error, 'organization');
// Show the first validation error to the user
const firstError = Object.values(this.errors)[0];
if (firstError) {
snackbar.error(firstError);
}
return; return;
} }
@@ -423,9 +426,41 @@ class OrgApi extends BaseApiWithErrors {
onError: (error) => { onError: (error) => {
console.error('Error updating organization:', error); console.error('Error updating organization:', error);
const message = error instanceof Error ? error.message : `${error}`; if (typeof error === 'object' && error !== null && 'error' in error) {
this.errors.general = message; const apiError = error as {
success: false;
error: string;
code?: string;
field?: string;
};
if (apiError.field === 'siteName' || apiError.code === 'SITENAME_EXISTS') {
const message = typeof apiError.error === 'string' ? apiError.error : 'Site name already exists';
this.errors = { ...this.errors, siteName: message };
snackbar.error(message);
return;
}
if (apiError.field && typeof apiError.error === 'string') {
this.errors = { ...this.errors, [apiError.field]: apiError.error };
snackbar.error(apiError.error);
return;
}
if (typeof apiError.error === 'string') {
this.errors = { ...this.errors, general: apiError.error };
snackbar.error(apiError.error);
return;
}
this.errors = { ...this.errors, general: t.get('snackbar.update_failed') };
snackbar.error(t.get('snackbar.update_failed'));
return;
}
const message =
error instanceof Error ? error.message : typeof error === 'string' ? error : t.get('snackbar.update_failed');
this.errors = { ...this.errors, general: message };
snackbar.error(`${t.get('snackbar.update_failed')}: ${message}`); snackbar.error(`${t.get('snackbar.update_failed')}: ${message}`);
} }
}); });
@@ -141,6 +141,8 @@
if (orgApi.success) { if (orgApi.success) {
$currentOrg.siteName = siteName; $currentOrg.siteName = siteName;
goto(`/org/${$currentOrg.siteName}/settings/domains`); goto(`/org/${$currentOrg.siteName}/settings/domains`);
} else if (orgApi.errors.siteName) {
errors.siteName = orgApi.errors.siteName;
} else if (orgApi.errors.general) { } else if (orgApi.errors.general) {
errors.siteName = orgApi.errors.general; errors.siteName = orgApi.errors.general;
} else { } else {
@@ -126,15 +126,25 @@ export class BaseApi {
export class BaseApiWithErrors extends BaseApi { export class BaseApiWithErrors extends BaseApi {
errors = $state<Record<string, string>>({}); errors = $state<Record<string, string>>({});
handleValidationError(result: { success: false; error: string; code: string; field?: string }): Promise<void> | void { handleValidationError(result: {
success: false;
error: string;
code?: string;
field?: string;
}): Promise<void> | void {
const zodValidationError = result.error as unknown as ZodValidationError; const zodValidationError = result.error as unknown as ZodValidationError;
if (zodValidationError.name === 'ZodError') {
// Handle Zod validation errors
if (zodValidationError && typeof zodValidationError === 'object' && zodValidationError.name === 'ZodError') {
const issues = JSON.parse(zodValidationError.message) as ZodValidatorMesssage; const issues = JSON.parse(zodValidationError.message) as ZodValidatorMesssage;
this.errors = mapZodErrorsToTranslations(new ZodError(issues)); this.errors = mapZodErrorsToTranslations(new ZodError(issues));
return; return;
} }
// Generic fallback only — no field-specific mapping here
if (typeof result.error === 'string') {
this.errors = { ...this.errors, general: result.error };
}
} }
override resetErrors() { override resetErrors() {
@@ -102,6 +102,7 @@
suffix=".classroomio.com" suffix=".classroomio.com"
oninput={() => { oninput={() => {
isSiteNameTouched = true; isSiteNameTouched = true;
onboardingApi.errors.siteName = '';
}} }}
/> />
{#if onboardingApi.errors.siteName} {#if onboardingApi.errors.siteName}
@@ -19,6 +19,8 @@ import {
ilike, ilike,
inArray, inArray,
isNotNull, isNotNull,
ne,
not,
notInArray, notInArray,
or, or,
sql sql
@@ -214,15 +216,19 @@ export async function insertOrganizationMembersOnConflictDoNothing(
} }
} }
export const checkSiteNameExists = async (siteName: string): Promise<boolean> => { export const checkSiteNameExists = async (siteName: string, excludeOrgId?: string): Promise<boolean> => {
const conditions = [eq(schema.organization.siteName, siteName)];
if (excludeOrgId) {
conditions.push(ne(schema.organization.id, excludeOrgId));
}
const result = await db const result = await db
.select({ id: schema.organization.id }) .select({ id: schema.organization.id })
.from(schema.organization) .from(schema.organization)
.where(eq(schema.organization.siteName, siteName)) .where(and(...conditions))
.limit(1); .limit(1);
console.debug('checkSiteNameExists result:', result);
return result.length > 0; return result.length > 0;
}; };