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 <best@classroomio.com>
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 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user