URL Configuration
-
+
{module.framework || 'N/A'}
-
-
{module.base_url}
+
+
{module.frontend_base_url || 'N/A'}
+
+
+
+
{module.backend_base_url || 'N/A'}
diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx
index b5370a8..bd7aa26 100644
--- a/src/pages/superadmin/CreateTenantWizard.tsx
+++ b/src/pages/superadmin/CreateTenantWizard.tsx
@@ -34,13 +34,14 @@ const tenantDetailsSchema = z.object({
}).optional().nullable(),
max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(),
max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(),
- modules: z.array(z.string().uuid()).optional().nullable(),
+ modules: z.array(z.uuid()).optional().nullable(),
});
// Step 2: Contact Details Schema - user creation + organization address
const contactDetailsSchema = z
.object({
- email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
+ email: z
+ .email({ message: 'Please enter a valid email address' }),
password: z.string().min(1, 'Password is required').min(6, 'Password must be at least 6 characters'),
confirmPassword: z.string().min(1, 'Confirm password is required'),
first_name: z.string().min(1, 'First name is required'),
@@ -113,6 +114,8 @@ const CreateTenantWizard = (): ReactElement => {
// Form instances for each step
const tenantDetailsForm = useForm({
resolver: zodResolver(tenantDetailsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
name: '',
slug: '',
@@ -139,6 +142,8 @@ const CreateTenantWizard = (): ReactElement => {
const contactDetailsForm = useForm({
resolver: zodResolver(contactDetailsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
email: '',
password: '',
@@ -157,6 +162,8 @@ const CreateTenantWizard = (): ReactElement => {
const settingsForm = useForm({
resolver: zodResolver(settingsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
enable_sso: false,
enable_2fa: false,
@@ -331,14 +338,18 @@ const CreateTenantWizard = (): ReactElement => {
setLogoError(null);
setFaviconError(null);
- if (!logoFileUrl && !logoFile) {
+ const isLogoMissing = !logoFileUrl && !logoFile;
+ const isFaviconMissing = !faviconFileUrl && !faviconFile;
+
+ if (isLogoMissing) {
setLogoError('Logo is required');
- setCurrentStep(3); // Go to settings step where logo/favicon are
- return;
}
- if (!faviconFileUrl && !faviconFile) {
+ if (isFaviconMissing) {
setFaviconError('Favicon is required');
+ }
+
+ if (isLogoMissing || isFaviconMissing) {
setCurrentStep(3); // Go to settings step where logo/favicon are
return;
}
@@ -379,40 +390,122 @@ const CreateTenantWizard = (): ReactElement => {
showToast.success(message);
navigate('/tenants');
} catch (err: any) {
- // Handle validation errors from API - same as NewTenantModal
+ // Clear previous errors
+ tenantDetailsForm.clearErrors();
+ contactDetailsForm.clearErrors();
+ settingsForm.clearErrors();
+
+ // Handle validation errors from API
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
const validationErrors = err.response.data.details;
+ let hasTenantErrors = false;
+ let hasContactErrors = false;
+ let hasSettingsErrors = false;
+
validationErrors.forEach((detail: { path: string; message: string }) => {
- // Handle tenant details errors
- if (
- detail.path === 'name' ||
- detail.path === 'slug' ||
- detail.path === 'status' ||
- detail.path === 'subscription_tier' ||
- detail.path === 'max_users' ||
- detail.path === 'max_modules' ||
- detail.path === 'module_ids'
+ const path = detail.path;
+
+ // Handle nested paths first
+ if (path.startsWith('settings.contact.')) {
+ // Contact details errors from nested path
+ hasContactErrors = true;
+ const fieldName = path.replace('settings.contact.', '');
+ if (fieldName === 'confirmPassword') {
+ // Skip confirmPassword as it's not in the form schema
+ return;
+ }
+ contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ } else if (path.startsWith('settings.branding.')) {
+ // Settings/branding errors from nested path
+ hasSettingsErrors = true;
+ const fieldName = path.replace('settings.branding.', '');
+ // Map file_path fields to form fields
+ if (fieldName === 'logo_file_path') {
+ setLogoError(detail.message);
+ } else if (fieldName === 'favicon_file_path') {
+ setFaviconError(detail.message);
+ } else {
+ settingsForm.setError(fieldName as keyof SettingsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ } else if (path.startsWith('settings.')) {
+ // Other settings errors
+ hasSettingsErrors = true;
+ const fieldName = path.replace('settings.', '');
+ settingsForm.setError(fieldName as keyof SettingsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ // Handle tenant details errors (step 1)
+ else if (
+ path === 'name' ||
+ path === 'slug' ||
+ path === 'domain' ||
+ path === 'status' ||
+ path === 'subscription_tier' ||
+ path === 'max_users' ||
+ path === 'max_modules' ||
+ path === 'module_ids'
) {
- const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path;
+ hasTenantErrors = true;
+ const fieldPath = path === 'module_ids' ? 'modules' : path;
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
type: 'server',
message: detail.message,
});
}
- // Handle contact details errors
+ // Handle contact details errors (step 2) - direct paths
else if (
- detail.path === 'email' ||
- detail.path === 'password' ||
- detail.path === 'first_name' ||
- detail.path === 'last_name'
+ path === 'email' ||
+ path === 'password' ||
+ path === 'first_name' ||
+ path === 'last_name' ||
+ path === 'contact_phone' ||
+ path === 'address_line1' ||
+ path === 'address_line2' ||
+ path === 'city' ||
+ path === 'state' ||
+ path === 'postal_code' ||
+ path === 'country'
) {
- contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, {
+ hasContactErrors = true;
+ contactDetailsForm.setError(path as keyof ContactDetailsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ // Handle settings errors (step 3) - direct paths
+ else if (
+ path === 'enable_sso' ||
+ path === 'enable_2fa' ||
+ path === 'primary_color' ||
+ path === 'secondary_color' ||
+ path === 'accent_color'
+ ) {
+ hasSettingsErrors = true;
+ settingsForm.setError(path as keyof SettingsForm, {
type: 'server',
message: detail.message,
});
}
});
+
+ // Navigate to the step with errors
+ if (hasTenantErrors) {
+ setCurrentStep(1);
+ } else if (hasContactErrors) {
+ setCurrentStep(2);
+ } else if (hasSettingsErrors) {
+ setCurrentStep(3);
+ }
} else {
+ // Handle general errors
const errorObj = err?.response?.data?.error;
const errorMessage =
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
@@ -425,6 +518,7 @@ const CreateTenantWizard = (): ReactElement => {
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.',
});
showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to create tenant. Please try again.');
+ setCurrentStep(1); // Navigate to first step for general errors
}
} finally {
setIsSubmitting(false);
@@ -778,6 +872,12 @@ const CreateTenantWizard = (): ReactElement => {
Set resource limits and security preferences for this tenant.
+ {/* General Error Display */}
+ {settingsForm.formState.errors.root && (
+
+
{settingsForm.formState.errors.root.message}
+
+ )}
{/* Branding Section */}
diff --git a/src/pages/superadmin/EditTenant.tsx b/src/pages/superadmin/EditTenant.tsx
index 8004cc3..0b5507e 100644
--- a/src/pages/superadmin/EditTenant.tsx
+++ b/src/pages/superadmin/EditTenant.tsx
@@ -34,12 +34,12 @@ const tenantDetailsSchema = z.object({
}).optional().nullable(),
max_users: z.number().int().min(1, 'max_users must be at least 1').optional().nullable(),
max_modules: z.number().int().min(1, 'max_modules must be at least 1').optional().nullable(),
- modules: z.array(z.string().uuid()).optional().nullable(),
+ modules: z.array(z.uuid()).optional().nullable(),
});
// Step 2: Contact Details Schema - NO password fields
const contactDetailsSchema = z.object({
- email: z.string().min(1, 'Email is required').email('Please enter a valid email address'),
+ email: z.email({ message: 'Please enter a valid email address' }),
first_name: z.string().min(1, 'First name is required'),
last_name: z.string().min(1, 'Last name is required'),
contact_phone: z
@@ -122,6 +122,8 @@ const EditTenant = (): ReactElement => {
// Form instances for each step
const tenantDetailsForm = useForm
({
resolver: zodResolver(tenantDetailsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
name: '',
slug: '',
@@ -136,6 +138,8 @@ const EditTenant = (): ReactElement => {
const contactDetailsForm = useForm({
resolver: zodResolver(contactDetailsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
email: '',
first_name: '',
@@ -152,6 +156,8 @@ const EditTenant = (): ReactElement => {
const settingsForm = useForm({
resolver: zodResolver(settingsSchema),
+ mode: 'onChange',
+ reValidateMode: 'onChange',
defaultValues: {
enable_sso: false,
enable_2fa: false,
@@ -433,14 +439,18 @@ const EditTenant = (): ReactElement => {
setLogoError(null);
setFaviconError(null);
- if (!logoFilePath) {
+ const isLogoMissing = !logoFilePath;
+ const isFaviconMissing = !faviconFilePath;
+
+ if (isLogoMissing) {
setLogoError('Logo is required');
- setCurrentStep(3); // Go to settings step where logo/favicon are
- return;
}
- if (!faviconFilePath) {
+ if (isFaviconMissing) {
setFaviconError('Favicon is required');
+ }
+
+ if (isLogoMissing || isFaviconMissing) {
setCurrentStep(3); // Go to settings step where logo/favicon are
return;
}
@@ -476,47 +486,130 @@ const EditTenant = (): ReactElement => {
showToast.success(message);
navigate('/tenants');
} catch (err: any) {
+ // Clear previous errors
+ tenantDetailsForm.clearErrors();
+ contactDetailsForm.clearErrors();
+ settingsForm.clearErrors();
+
+ // Handle validation errors from API
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
const validationErrors = err.response.data.details;
+ let hasTenantErrors = false;
+ let hasContactErrors = false;
+ let hasSettingsErrors = false;
+
validationErrors.forEach((detail: { path: string; message: string }) => {
- if (
- detail.path === 'name' ||
- detail.path === 'slug' ||
- detail.path === 'status' ||
- detail.path === 'subscription_tier' ||
- detail.path === 'max_users' ||
- detail.path === 'max_modules' ||
- detail.path === 'module_ids'
+ const path = detail.path;
+
+ // Handle nested paths first
+ if (path.startsWith('settings.contact.')) {
+ // Contact details errors from nested path
+ hasContactErrors = true;
+ const fieldName = path.replace('settings.contact.', '');
+ contactDetailsForm.setError(fieldName as keyof ContactDetailsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ } else if (path.startsWith('settings.branding.')) {
+ // Settings/branding errors from nested path
+ hasSettingsErrors = true;
+ const fieldName = path.replace('settings.branding.', '');
+ // Map file_path fields to form fields
+ if (fieldName === 'logo_file_path') {
+ setLogoError(detail.message);
+ } else if (fieldName === 'favicon_file_path') {
+ setFaviconError(detail.message);
+ } else {
+ settingsForm.setError(fieldName as keyof SettingsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ } else if (path.startsWith('settings.')) {
+ // Other settings errors
+ hasSettingsErrors = true;
+ const fieldName = path.replace('settings.', '');
+ settingsForm.setError(fieldName as keyof SettingsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ // Handle tenant details errors (step 1)
+ else if (
+ path === 'name' ||
+ path === 'slug' ||
+ path === 'domain' ||
+ path === 'status' ||
+ path === 'subscription_tier' ||
+ path === 'max_users' ||
+ path === 'max_modules' ||
+ path === 'module_ids'
) {
- const fieldPath = detail.path === 'module_ids' ? 'modules' : detail.path;
+ hasTenantErrors = true;
+ const fieldPath = path === 'module_ids' ? 'modules' : path;
tenantDetailsForm.setError(fieldPath as keyof TenantDetailsForm, {
type: 'server',
message: detail.message,
});
- } else if (
- detail.path === 'email' ||
- detail.path === 'first_name' ||
- detail.path === 'last_name' ||
- detail.path === 'contact_phone' ||
- detail.path === 'address_line1' ||
- detail.path === 'city' ||
- detail.path === 'state' ||
- detail.path === 'postal_code' ||
- detail.path === 'country'
+ }
+ // Handle contact details errors (step 2) - direct paths
+ else if (
+ path === 'email' ||
+ path === 'first_name' ||
+ path === 'last_name' ||
+ path === 'contact_phone' ||
+ path === 'address_line1' ||
+ path === 'address_line2' ||
+ path === 'city' ||
+ path === 'state' ||
+ path === 'postal_code' ||
+ path === 'country'
) {
- contactDetailsForm.setError(detail.path as keyof ContactDetailsForm, {
+ hasContactErrors = true;
+ contactDetailsForm.setError(path as keyof ContactDetailsForm, {
+ type: 'server',
+ message: detail.message,
+ });
+ }
+ // Handle settings errors (step 3) - direct paths
+ else if (
+ path === 'enable_sso' ||
+ path === 'enable_2fa' ||
+ path === 'primary_color' ||
+ path === 'secondary_color' ||
+ path === 'accent_color'
+ ) {
+ hasSettingsErrors = true;
+ settingsForm.setError(path as keyof SettingsForm, {
type: 'server',
message: detail.message,
});
}
});
+
+ // Navigate to the step with errors
+ if (hasTenantErrors) {
+ setCurrentStep(1);
+ } else if (hasContactErrors) {
+ setCurrentStep(2);
+ } else if (hasSettingsErrors) {
+ setCurrentStep(3);
+ }
} else {
+ // Handle general errors
+ const errorObj = err?.response?.data?.error;
const errorMessage =
- err?.response?.data?.error?.message ||
+ (typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
+ (typeof errorObj === 'string' ? errorObj : null) ||
err?.response?.data?.message ||
err?.message ||
'Failed to update tenant. Please try again.';
- showToast.error(errorMessage);
+ tenantDetailsForm.setError('root', {
+ type: 'server',
+ message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.',
+ });
+ showToast.error(typeof errorMessage === 'string' ? errorMessage : 'Failed to update tenant. Please try again.');
+ setCurrentStep(1); // Navigate to first step for general errors
}
} finally {
setIsSubmitting(false);
@@ -878,6 +971,12 @@ const EditTenant = (): ReactElement => {
Configuration & Limits
Set resource limits and security preferences for this tenant.
+ {/* General Error Display */}
+ {settingsForm.formState.errors.root && (
+
+
{settingsForm.formState.errors.root.message}
+
+ )}
{/* Branding Section - Same as CreateTenantWizard */}
diff --git a/src/pages/tenant/Settings.tsx b/src/pages/tenant/Settings.tsx
index 051e83f..05ad0f1 100644
--- a/src/pages/tenant/Settings.tsx
+++ b/src/pages/tenant/Settings.tsx
@@ -245,13 +245,18 @@ const Settings = (): ReactElement => {
setLogoError(null);
setFaviconError(null);
- if (!logoFilePath) {
+ const isLogoMissing = !logoFilePath;
+ const isFaviconMissing = !faviconFilePath;
+
+ if (isLogoMissing) {
setLogoError('Logo is required');
- return;
}
- if (!faviconFilePath) {
+ if (isFaviconMissing) {
setFaviconError('Favicon is required');
+ }
+
+ if (isLogoMissing || isFaviconMissing) {
return;
}
diff --git a/src/types/module.ts b/src/types/module.ts
index ca5a88f..519b07b 100644
--- a/src/types/module.ts
+++ b/src/types/module.ts
@@ -8,7 +8,8 @@ export interface Module {
runtime_language: string | null;
framework: string | null;
webhookurl: string | null;
- base_url: string;
+ frontend_base_url: string;
+ backend_base_url: string;
health_endpoint: string;
endpoints: string[] | null;
kafka_topics: string[] | null;