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:
co-authored by
Rotimi Best
parent
8ad0141ab8
commit
a83e172e6d
@@ -4,6 +4,7 @@ import type { TGetAudienceQuery, TGetOrganizationCoursesQuery } from '@cio/utils
|
||||
import type { TNewOrganizationPlan, TOrganization, TOrganizationPlan } from '@db/types';
|
||||
import {
|
||||
cancelOrganizationPlan,
|
||||
checkSiteNameExists,
|
||||
createOrganizationPlan,
|
||||
deleteOrganizationAudienceMember,
|
||||
deleteOrganizationMember,
|
||||
@@ -648,7 +649,13 @@ export async function createOrgPlan(data: TNewOrganizationPlan) {
|
||||
*/
|
||||
export async function updateOrg(orgId: string, data: Partial<TOrganization>) {
|
||||
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') {
|
||||
const landingpage = data.landingpage as Record<string, unknown>;
|
||||
const theme = landingpage.theme;
|
||||
|
||||
@@ -55,8 +55,19 @@ export class OnboardingApi extends BaseApiWithErrors {
|
||||
return;
|
||||
}
|
||||
|
||||
// map api validation back to frontend fields
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
import { DEFAULT_ORG_AUDIENCE_QUERY, toAudienceRequestQuery } from '../utils/audience-query-utils';
|
||||
import type { ZodError } from 'zod';
|
||||
|
||||
export interface TOrgUpdateForm {
|
||||
name?: string;
|
||||
@@ -309,11 +310,8 @@ class OrgApi extends BaseApiWithErrors {
|
||||
snackbar.error(result);
|
||||
return;
|
||||
}
|
||||
if ('error' in result && 'field' in result) {
|
||||
// Field-specific error automatically mapped to this.errors[field]
|
||||
this.errors[result.field as string] = result.error;
|
||||
} else if ('error' in result) {
|
||||
this.errors.general = result.error;
|
||||
if ('error' in result) {
|
||||
this.handleValidationError(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -340,6 +338,11 @@ class OrgApi extends BaseApiWithErrors {
|
||||
|
||||
if (!result.success) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -423,9 +426,41 @@ class OrgApi extends BaseApiWithErrors {
|
||||
onError: (error) => {
|
||||
console.error('Error updating organization:', error);
|
||||
|
||||
const message = error instanceof Error ? error.message : `${error}`;
|
||||
this.errors.general = message;
|
||||
if (typeof error === 'object' && error !== null && 'error' in error) {
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
if (orgApi.success) {
|
||||
$currentOrg.siteName = siteName;
|
||||
goto(`/org/${$currentOrg.siteName}/settings/domains`);
|
||||
} else if (orgApi.errors.siteName) {
|
||||
errors.siteName = orgApi.errors.siteName;
|
||||
} else if (orgApi.errors.general) {
|
||||
errors.siteName = orgApi.errors.general;
|
||||
} else {
|
||||
|
||||
@@ -126,15 +126,25 @@ export class BaseApi {
|
||||
export class BaseApiWithErrors extends BaseApi {
|
||||
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;
|
||||
if (zodValidationError.name === 'ZodError') {
|
||||
|
||||
// Handle Zod validation errors
|
||||
if (zodValidationError && typeof zodValidationError === 'object' && zodValidationError.name === 'ZodError') {
|
||||
const issues = JSON.parse(zodValidationError.message) as ZodValidatorMesssage;
|
||||
|
||||
this.errors = mapZodErrorsToTranslations(new ZodError(issues));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic fallback only — no field-specific mapping here
|
||||
if (typeof result.error === 'string') {
|
||||
this.errors = { ...this.errors, general: result.error };
|
||||
}
|
||||
}
|
||||
|
||||
override resetErrors() {
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
suffix=".classroomio.com"
|
||||
oninput={() => {
|
||||
isSiteNameTouched = true;
|
||||
onboardingApi.errors.siteName = '';
|
||||
}}
|
||||
/>
|
||||
{#if onboardingApi.errors.siteName}
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
ilike,
|
||||
inArray,
|
||||
isNotNull,
|
||||
ne,
|
||||
not,
|
||||
notInArray,
|
||||
or,
|
||||
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
|
||||
.select({ id: schema.organization.id })
|
||||
.from(schema.organization)
|
||||
.where(eq(schema.organization.siteName, siteName))
|
||||
.where(and(...conditions))
|
||||
.limit(1);
|
||||
|
||||
console.debug('checkSiteNameExists result:', result);
|
||||
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user