feat: Implement authenticated image handling for tenant logos and favicons, utilizing file attachment UUIDs and a new AuthenticatedImage component.
This commit is contained in:
parent
757a9f216b
commit
5b03dec1d8
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_FRONTEND_BASE_URL=
|
||||||
|
VITE_API_BASE_URL=
|
||||||
@ -80,6 +80,7 @@ A modern, responsive admin dashboard for managing tenants, users, roles, and sys
|
|||||||
Edit `.env` and add your configuration:
|
Edit `.env` and add your configuration:
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||||
|
VITE_FRONTEND_BASE_URL=http://localhost:5173
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Start the development server**
|
4. **Start the development server**
|
||||||
@ -123,6 +124,7 @@ Create a `.env` file in the root directory:
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE_URL=your-api-base-url
|
VITE_API_BASE_URL=your-api-base-url
|
||||||
|
VITE_FRONTEND_BASE_URL=your-frontend-base-url
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Design System
|
## 🎨 Design System
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAppSelector } from '@/hooks/redux-hooks';
|
import { useAppSelector } from '@/hooks/redux-hooks';
|
||||||
import { useTenantTheme } from '@/hooks/useTenantTheme';
|
import { useTenantTheme } from '@/hooks/useTenantTheme';
|
||||||
|
import { AuthenticatedImage } from '@/components/shared';
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
@ -221,15 +222,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
{!isSuperAdmin && logoUrl ? (
|
{!isSuperAdmin && logoUrl ? (
|
||||||
<img
|
<AuthenticatedImage
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
className="h-9 w-auto max-w-[180px] object-contain"
|
className="h-9 w-auto max-w-[180px] object-contain"
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
const fallback = e.currentTarget.nextElementSibling as HTMLElement;
|
|
||||||
if (fallback) fallback.style.display = 'flex';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
|
|||||||
104
src/components/shared/AuthenticatedImage.tsx
Normal file
104
src/components/shared/AuthenticatedImage.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { useState, useEffect, type ImgHTMLAttributes, type ReactElement } from 'react';
|
||||||
|
import { fileService } from '@/services/file-service';
|
||||||
|
import apiClient from '@/services/api-client';
|
||||||
|
import { Loader2, ImageIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AuthenticatedImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
||||||
|
fileId?: string | null;
|
||||||
|
src?: string | null;
|
||||||
|
fallback?: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthenticatedImage = ({
|
||||||
|
fileId,
|
||||||
|
src,
|
||||||
|
fallback,
|
||||||
|
className,
|
||||||
|
alt = 'Image',
|
||||||
|
...props
|
||||||
|
}: AuthenticatedImageProps): ReactElement => {
|
||||||
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If it's already a blob URL (local preview) or a data URL, use it directly
|
||||||
|
if (src && (src.startsWith('blob:') || src.startsWith('data:'))) {
|
||||||
|
setBlobUrl(src);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||||
|
const isBackendUrl = src && src.includes(`${baseUrl}/files/`) && src.includes('/preview');
|
||||||
|
|
||||||
|
// If we have a fileId or a backend URL, fetch it via authenticated request
|
||||||
|
if (fileId || isBackendUrl) {
|
||||||
|
let isMounted = true;
|
||||||
|
const fetchImage = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(false);
|
||||||
|
try {
|
||||||
|
let url: string;
|
||||||
|
if (fileId) {
|
||||||
|
url = await fileService.getPreview(fileId);
|
||||||
|
} else if (src) {
|
||||||
|
const response = await apiClient.get(src, { responseType: 'blob' });
|
||||||
|
url = URL.createObjectURL(response.data);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setBlobUrl(url);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch authenticated image:', err);
|
||||||
|
if (isMounted) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchImage();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
if (blobUrl && blobUrl.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else if (src) {
|
||||||
|
// For other external URLs, use them directly
|
||||||
|
setBlobUrl(src);
|
||||||
|
}
|
||||||
|
}, [fileId, src]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}>
|
||||||
|
<Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || (!blobUrl && !src)) {
|
||||||
|
return fallback || (
|
||||||
|
<div className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}>
|
||||||
|
<ImageIcon className="w-5 h-5 text-[#9ca3af]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={blobUrl || src || ''}
|
||||||
|
className={className}
|
||||||
|
alt={alt}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -21,4 +21,5 @@ export { ViewRoleModal } from './ViewRoleModal';
|
|||||||
export { EditRoleModal } from './EditRoleModal';
|
export { EditRoleModal } from './EditRoleModal';
|
||||||
export { ViewAuditLogModal } from './ViewAuditLogModal';
|
export { ViewAuditLogModal } from './ViewAuditLogModal';
|
||||||
export { PageHeader } from './PageHeader';
|
export { PageHeader } from './PageHeader';
|
||||||
export type { TabItem } from './PageHeader';
|
export type { TabItem } from './PageHeader';
|
||||||
|
export { AuthenticatedImage } from './AuthenticatedImage';
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks';
|
||||||
import { fetchThemeAsync } from '@/store/themeSlice';
|
import { fetchThemeAsync } from '@/store/themeSlice';
|
||||||
|
import apiClient from '@/services/api-client';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to fetch and apply tenant theme
|
* Hook to fetch and apply tenant theme
|
||||||
@ -19,17 +20,53 @@ export const useTenantTheme = (): void => {
|
|||||||
|
|
||||||
// Apply favicon
|
// Apply favicon
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (faviconUrl) {
|
if (!faviconUrl) return;
|
||||||
// Remove existing favicon links
|
|
||||||
const existingFavicons = document.querySelectorAll("link[rel='icon'], link[rel='shortcut icon']");
|
|
||||||
existingFavicons.forEach((favicon) => favicon.remove());
|
|
||||||
|
|
||||||
// Add new favicon
|
let isMounted = true;
|
||||||
const link = document.createElement('link');
|
let blobUrl: string | null = null;
|
||||||
link.rel = 'icon';
|
|
||||||
link.type = 'image/png';
|
const applyFavicon = async () => {
|
||||||
link.href = faviconUrl;
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1';
|
||||||
document.head.appendChild(link);
|
const isBackendUrl = faviconUrl.includes(`${baseUrl}/files/`) && faviconUrl.includes('/preview');
|
||||||
}
|
|
||||||
|
let finalUrl = faviconUrl;
|
||||||
|
|
||||||
|
// If it's a backend URL, fetch it with authentication
|
||||||
|
if (isBackendUrl) {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(faviconUrl, { responseType: 'blob' });
|
||||||
|
if (isMounted) {
|
||||||
|
blobUrl = URL.createObjectURL(response.data);
|
||||||
|
finalUrl = blobUrl;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch authenticated favicon:', err);
|
||||||
|
// Fallback to original URL, although it might still fail at browser level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
// Remove existing favicon links
|
||||||
|
const existingFavicons = document.querySelectorAll("link[rel='icon'], link[rel='shortcut icon']");
|
||||||
|
existingFavicons.forEach((favicon) => favicon.remove());
|
||||||
|
|
||||||
|
// Add new favicon
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'icon';
|
||||||
|
// Try to detect type or default to image/png
|
||||||
|
link.type = faviconUrl.endsWith('.ico') ? 'image/x-icon' : 'image/png';
|
||||||
|
link.href = finalUrl;
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
applyFavicon();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
if (blobUrl) {
|
||||||
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [faviconUrl]);
|
}, [faviconUrl]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { Layout } from '@/components/layout/Layout';
|
||||||
import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared';
|
import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect, AuthenticatedImage } from '@/components/shared';
|
||||||
import { tenantService } from '@/services/tenant-service';
|
import { tenantService } from '@/services/tenant-service';
|
||||||
import { moduleService } from '@/services/module-service';
|
import { moduleService } from '@/services/module-service';
|
||||||
import { fileService } from '@/services/file-service';
|
import { fileService } from '@/services/file-service';
|
||||||
@ -100,7 +100,7 @@ const subscriptionTierOptions = [
|
|||||||
|
|
||||||
// Helper function to get base URL with protocol
|
// Helper function to get base URL with protocol
|
||||||
const getBaseUrlWithProtocol = (): string => {
|
const getBaseUrlWithProtocol = (): string => {
|
||||||
return import.meta.env.VITE_FRONTEND_BASE_URL || 'http://localhost:5173';
|
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateTenantWizard = (): ReactElement => {
|
const CreateTenantWizard = (): ReactElement => {
|
||||||
@ -176,10 +176,10 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
// File upload state for branding
|
// File upload state for branding
|
||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||||
// const [logoFilePath, setLogoFilePath] = useState<string | null>(null);
|
const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState<string | null>(null);
|
||||||
|
const [faviconFileAttachmentUuid, setFaviconFileAttachmentUuid] = useState<string | null>(null);
|
||||||
const [logoFileUrl, setLogoFileUrl] = useState<string | null>(null);
|
const [logoFileUrl, setLogoFileUrl] = useState<string | null>(null);
|
||||||
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
||||||
// const [faviconFilePath, setFaviconFilePath] = useState<string | null>(null);
|
|
||||||
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
||||||
const [faviconPreviewUrl, setFaviconPreviewUrl] = useState<string | null>(null);
|
const [faviconPreviewUrl, setFaviconPreviewUrl] = useState<string | null>(null);
|
||||||
const [isUploadingLogo, setIsUploadingLogo] = useState<boolean>(false);
|
const [isUploadingLogo, setIsUploadingLogo] = useState<boolean>(false);
|
||||||
@ -307,6 +307,7 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
setLogoFile(null);
|
setLogoFile(null);
|
||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFileUrl(null);
|
setLogoFileUrl(null);
|
||||||
|
setLogoFileAttachmentUuid(null);
|
||||||
setLogoError(null);
|
setLogoError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('logo-upload-wizard') as HTMLInputElement;
|
const fileInput = document.getElementById('logo-upload-wizard') as HTMLInputElement;
|
||||||
@ -322,6 +323,7 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
setFaviconFile(null);
|
setFaviconFile(null);
|
||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
|
setFaviconFileAttachmentUuid(null);
|
||||||
setFaviconError(null);
|
setFaviconError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('favicon-upload-wizard') as HTMLInputElement;
|
const fileInput = document.getElementById('favicon-upload-wizard') as HTMLInputElement;
|
||||||
@ -380,7 +382,9 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
secondary_color: secondary_color || undefined,
|
secondary_color: secondary_color || undefined,
|
||||||
accent_color: accent_color || undefined,
|
accent_color: accent_color || undefined,
|
||||||
logo_file_path: logoFileUrl || undefined,
|
logo_file_path: logoFileUrl || undefined,
|
||||||
|
logo_file_attachment_uuid: logoFileAttachmentUuid || undefined,
|
||||||
favicon_file_path: faviconFileUrl || undefined,
|
favicon_file_path: faviconFileUrl || undefined,
|
||||||
|
favicon_file_attachment_uuid: faviconFileAttachmentUuid || undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -936,11 +940,16 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
setLogoPreviewUrl(previewUrl);
|
setLogoPreviewUrl(previewUrl);
|
||||||
setIsUploadingLogo(true);
|
setIsUploadingLogo(true);
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const slug = tenantDetailsForm.getValues('slug');
|
||||||
// setLogoFilePath(response.data.file_path);
|
const response = await fileService.upload(file, slug || 'tenant');
|
||||||
setLogoFileUrl(response.data.file_url);
|
const fileId = response.data.id;
|
||||||
setLogoError(null); // Clear error on successful upload
|
setLogoFileAttachmentUuid(fileId);
|
||||||
// Keep preview URL as fallback, will be cleaned up on component unmount or file change
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setLogoFileUrl(formattedUrl);
|
||||||
|
|
||||||
|
setLogoError(null);
|
||||||
showToast.success('Logo uploaded successfully');
|
showToast.success('Logo uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -950,11 +959,10 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
'Failed to upload logo. Please try again.';
|
'Failed to upload logo. Please try again.';
|
||||||
showToast.error(errorMessage);
|
showToast.error(errorMessage);
|
||||||
setLogoFile(null);
|
setLogoFile(null);
|
||||||
// Clean up preview URL on error
|
|
||||||
URL.revokeObjectURL(previewUrl);
|
URL.revokeObjectURL(previewUrl);
|
||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFileUrl(null);
|
setLogoFileUrl(null);
|
||||||
// setLogoFilePath(null);
|
setLogoFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingLogo(false);
|
setIsUploadingLogo(false);
|
||||||
}
|
}
|
||||||
@ -975,26 +983,13 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{(logoPreviewUrl || logoFileUrl) && (
|
{(logoPreviewUrl || logoFileUrl) && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
key={logoPreviewUrl || logoFileUrl}
|
key={logoPreviewUrl || logoFileUrl}
|
||||||
src={logoPreviewUrl || logoFileUrl || ''}
|
fileId={logoFileAttachmentUuid}
|
||||||
|
src={logoPreviewUrl || logoFileUrl}
|
||||||
alt="Logo preview"
|
alt="Logo preview"
|
||||||
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', maxHeight: '80px' }}
|
style={{ display: 'block', maxHeight: '80px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load logo preview image', {
|
|
||||||
logoFileUrl,
|
|
||||||
logoPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onLoad={() => {
|
|
||||||
console.log('Logo preview loaded successfully', {
|
|
||||||
logoFileUrl,
|
|
||||||
logoPreviewUrl,
|
|
||||||
src: logoFileUrl || logoPreviewUrl,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1055,11 +1050,16 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(previewUrl);
|
setFaviconPreviewUrl(previewUrl);
|
||||||
setIsUploadingFavicon(true);
|
setIsUploadingFavicon(true);
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const slug = tenantDetailsForm.getValues('slug');
|
||||||
// setFaviconFilePath(response.data.file_path);
|
const response = await fileService.upload(file, slug || 'tenant');
|
||||||
setFaviconFileUrl(response.data.file_url);
|
const fileId = response.data.id;
|
||||||
setFaviconError(null); // Clear error on successful upload
|
setFaviconFileAttachmentUuid(fileId);
|
||||||
// Keep preview URL as fallback, will be cleaned up on component unmount or file change
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setFaviconFileUrl(formattedUrl);
|
||||||
|
|
||||||
|
setFaviconError(null);
|
||||||
showToast.success('Favicon uploaded successfully');
|
showToast.success('Favicon uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -1069,11 +1069,10 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
'Failed to upload favicon. Please try again.';
|
'Failed to upload favicon. Please try again.';
|
||||||
showToast.error(errorMessage);
|
showToast.error(errorMessage);
|
||||||
setFaviconFile(null);
|
setFaviconFile(null);
|
||||||
// Clean up preview URL on error
|
|
||||||
URL.revokeObjectURL(previewUrl);
|
URL.revokeObjectURL(previewUrl);
|
||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
// setFaviconFilePath(null);
|
setFaviconFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingFavicon(false);
|
setIsUploadingFavicon(false);
|
||||||
}
|
}
|
||||||
@ -1094,26 +1093,13 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{(faviconPreviewUrl || faviconFileUrl) && (
|
{(faviconPreviewUrl || faviconFileUrl) && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
key={faviconFileUrl || faviconPreviewUrl || ''}
|
key={faviconFileUrl || faviconPreviewUrl || ''}
|
||||||
src={faviconPreviewUrl || faviconFileUrl || ''}
|
fileId={faviconFileAttachmentUuid}
|
||||||
|
src={faviconPreviewUrl || faviconFileUrl}
|
||||||
alt="Favicon preview"
|
alt="Favicon preview"
|
||||||
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', width: '64px', height: '64px' }}
|
style={{ display: 'block', width: '64px', height: '64px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load favicon preview image', {
|
|
||||||
faviconFileUrl,
|
|
||||||
faviconPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onLoad={() => {
|
|
||||||
console.log('Favicon preview loaded successfully', {
|
|
||||||
faviconFileUrl,
|
|
||||||
faviconPreviewUrl,
|
|
||||||
src: faviconFileUrl || faviconPreviewUrl,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Layout } from '@/components/layout/Layout';
|
import { Layout } from '@/components/layout/Layout';
|
||||||
import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared';
|
import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect, AuthenticatedImage } from '@/components/shared';
|
||||||
import { tenantService } from '@/services/tenant-service';
|
import { tenantService } from '@/services/tenant-service';
|
||||||
import { moduleService } from '@/services/module-service';
|
import { moduleService } from '@/services/module-service';
|
||||||
import { fileService } from '@/services/file-service';
|
import { fileService } from '@/services/file-service';
|
||||||
@ -92,7 +92,7 @@ const subscriptionTierOptions = [
|
|||||||
|
|
||||||
// Helper function to get base URL with protocol
|
// Helper function to get base URL with protocol
|
||||||
const getBaseUrlWithProtocol = (): string => {
|
const getBaseUrlWithProtocol = (): string => {
|
||||||
return import.meta.env.VITE_FRONTEND_BASE_URL || 'http://localhost:5173';
|
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||||
};
|
};
|
||||||
|
|
||||||
const EditTenant = (): ReactElement => {
|
const EditTenant = (): ReactElement => {
|
||||||
@ -110,6 +110,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||||
const [logoFilePath, setLogoFilePath] = useState<string | null>(null);
|
const [logoFilePath, setLogoFilePath] = useState<string | null>(null);
|
||||||
|
const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState<string | null>(null);
|
||||||
|
const [faviconFileAttachmentUuid, setFaviconFileAttachmentUuid] = useState<string | null>(null);
|
||||||
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
||||||
const [faviconFilePath, setFaviconFilePath] = useState<string | null>(null);
|
const [faviconFilePath, setFaviconFilePath] = useState<string | null>(null);
|
||||||
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
||||||
@ -243,20 +245,22 @@ const EditTenant = (): ReactElement => {
|
|||||||
const accentColor = tenant.accent_color || branding.accent_color || '#084CC8';
|
const accentColor = tenant.accent_color || branding.accent_color || '#084CC8';
|
||||||
const logoPath = tenant.logo_file_path || branding.logo_file_path || null;
|
const logoPath = tenant.logo_file_path || branding.logo_file_path || null;
|
||||||
const faviconPath = tenant.favicon_file_path || branding.favicon_file_path || null;
|
const faviconPath = tenant.favicon_file_path || branding.favicon_file_path || null;
|
||||||
|
const logoUuid = tenant.logo_file_attachment_uuid || branding.logo_file_attachment_uuid || null;
|
||||||
|
const faviconUuid = tenant.favicon_file_attachment_uuid || branding.favicon_file_attachment_uuid || null;
|
||||||
|
|
||||||
// Set file paths and URLs if they exist
|
// Set file paths and URLs if they exist
|
||||||
if (logoPath) {
|
if (logoPath) {
|
||||||
setLogoFilePath(logoPath);
|
setLogoFilePath(logoPath);
|
||||||
// const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1';
|
|
||||||
setLogoPreviewUrl(logoPath);
|
setLogoPreviewUrl(logoPath);
|
||||||
setLogoError(null); // Clear error if existing logo is found
|
setLogoFileAttachmentUuid(logoUuid);
|
||||||
|
setLogoError(null);
|
||||||
}
|
}
|
||||||
if (faviconPath) {
|
if (faviconPath) {
|
||||||
setFaviconFilePath(faviconPath);
|
setFaviconFilePath(faviconPath);
|
||||||
// const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api/v1';
|
|
||||||
setFaviconFileUrl(faviconPath);
|
setFaviconFileUrl(faviconPath);
|
||||||
setFaviconPreviewUrl(faviconPath);
|
setFaviconPreviewUrl(faviconPath);
|
||||||
setFaviconError(null); // Clear error if existing favicon is found
|
setFaviconFileAttachmentUuid(faviconUuid);
|
||||||
|
setFaviconError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate subscription_tier
|
// Validate subscription_tier
|
||||||
@ -405,7 +409,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
setLogoFile(null);
|
setLogoFile(null);
|
||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFilePath(null);
|
setLogoFilePath(null);
|
||||||
setLogoError(null); // Clear error on delete
|
setLogoFileAttachmentUuid(null);
|
||||||
|
setLogoError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('logo-upload-edit-page') as HTMLInputElement;
|
const fileInput = document.getElementById('logo-upload-edit-page') as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
@ -421,7 +426,8 @@ const EditTenant = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
setFaviconFilePath(null);
|
setFaviconFilePath(null);
|
||||||
setFaviconError(null); // Clear error on delete
|
setFaviconFileAttachmentUuid(null);
|
||||||
|
setFaviconError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('favicon-upload-edit-page') as HTMLInputElement;
|
const fileInput = document.getElementById('favicon-upload-edit-page') as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
@ -476,7 +482,9 @@ const EditTenant = (): ReactElement => {
|
|||||||
secondary_color: secondary_color || undefined,
|
secondary_color: secondary_color || undefined,
|
||||||
accent_color: accent_color || undefined,
|
accent_color: accent_color || undefined,
|
||||||
logo_file_path: logoFilePath || undefined,
|
logo_file_path: logoFilePath || undefined,
|
||||||
|
logo_file_attachment_uuid: logoFileAttachmentUuid || undefined,
|
||||||
favicon_file_path: faviconFilePath || undefined,
|
favicon_file_path: faviconFilePath || undefined,
|
||||||
|
favicon_file_attachment_uuid: faviconFileAttachmentUuid || undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -490,17 +498,17 @@ const EditTenant = (): ReactElement => {
|
|||||||
tenantDetailsForm.clearErrors();
|
tenantDetailsForm.clearErrors();
|
||||||
contactDetailsForm.clearErrors();
|
contactDetailsForm.clearErrors();
|
||||||
settingsForm.clearErrors();
|
settingsForm.clearErrors();
|
||||||
|
|
||||||
// Handle validation errors from API
|
// Handle validation errors from API
|
||||||
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
|
||||||
const validationErrors = err.response.data.details;
|
const validationErrors = err.response.data.details;
|
||||||
let hasTenantErrors = false;
|
let hasTenantErrors = false;
|
||||||
let hasContactErrors = false;
|
let hasContactErrors = false;
|
||||||
let hasSettingsErrors = false;
|
let hasSettingsErrors = false;
|
||||||
|
|
||||||
validationErrors.forEach((detail: { path: string; message: string }) => {
|
validationErrors.forEach((detail: { path: string; message: string }) => {
|
||||||
const path = detail.path;
|
const path = detail.path;
|
||||||
|
|
||||||
// Handle nested paths first
|
// Handle nested paths first
|
||||||
if (path.startsWith('settings.contact.')) {
|
if (path.startsWith('settings.contact.')) {
|
||||||
// Contact details errors from nested path
|
// Contact details errors from nested path
|
||||||
@ -586,7 +594,7 @@ const EditTenant = (): ReactElement => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navigate to the step with errors
|
// Navigate to the step with errors
|
||||||
if (hasTenantErrors) {
|
if (hasTenantErrors) {
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
@ -1029,9 +1037,17 @@ const EditTenant = (): ReactElement => {
|
|||||||
setLogoPreviewUrl(previewUrl);
|
setLogoPreviewUrl(previewUrl);
|
||||||
setIsUploadingLogo(true);
|
setIsUploadingLogo(true);
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const slug = tenantDetailsForm.getValues('slug');
|
||||||
setLogoFilePath(response.data.file_url);
|
const response = await fileService.upload(file, slug || 'tenant', id, id);
|
||||||
setLogoError(null); // Clear error on successful upload
|
const fileId = response.data.id;
|
||||||
|
setLogoFileAttachmentUuid(fileId);
|
||||||
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setLogoFilePath(formattedUrl);
|
||||||
|
setLogoPreviewUrl(formattedUrl);
|
||||||
|
|
||||||
|
setLogoError(null);
|
||||||
showToast.success('Logo uploaded successfully');
|
showToast.success('Logo uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -1044,6 +1060,7 @@ const EditTenant = (): ReactElement => {
|
|||||||
URL.revokeObjectURL(previewUrl);
|
URL.revokeObjectURL(previewUrl);
|
||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFilePath(null);
|
setLogoFilePath(null);
|
||||||
|
setLogoFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingLogo(false);
|
setIsUploadingLogo(false);
|
||||||
}
|
}
|
||||||
@ -1064,18 +1081,13 @@ const EditTenant = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{logoPreviewUrl && (
|
{logoPreviewUrl && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
key={logoPreviewUrl}
|
key={logoPreviewUrl}
|
||||||
src={logoPreviewUrl || ''}
|
fileId={logoFileAttachmentUuid}
|
||||||
|
src={logoPreviewUrl}
|
||||||
alt="Logo preview"
|
alt="Logo preview"
|
||||||
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', maxHeight: '80px' }}
|
style={{ display: 'block', maxHeight: '80px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load logo preview image', {
|
|
||||||
logoPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -1131,10 +1143,18 @@ const EditTenant = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(previewUrl);
|
setFaviconPreviewUrl(previewUrl);
|
||||||
setIsUploadingFavicon(true);
|
setIsUploadingFavicon(true);
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const slug = tenantDetailsForm.getValues('slug');
|
||||||
setFaviconFilePath(response.data.file_url);
|
const response = await fileService.upload(file, slug || 'tenant', id, id);
|
||||||
setFaviconFileUrl(response.data.file_url);
|
const fileId = response.data.id;
|
||||||
setFaviconError(null); // Clear error on successful upload
|
setFaviconFileAttachmentUuid(fileId);
|
||||||
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setFaviconFilePath(formattedUrl);
|
||||||
|
setFaviconFileUrl(formattedUrl);
|
||||||
|
setFaviconPreviewUrl(formattedUrl);
|
||||||
|
|
||||||
|
setFaviconError(null);
|
||||||
showToast.success('Favicon uploaded successfully');
|
showToast.success('Favicon uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -1148,6 +1168,7 @@ const EditTenant = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
setFaviconFilePath(null);
|
setFaviconFilePath(null);
|
||||||
|
setFaviconFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingFavicon(false);
|
setIsUploadingFavicon(false);
|
||||||
}
|
}
|
||||||
@ -1168,19 +1189,13 @@ const EditTenant = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{(faviconPreviewUrl || faviconFileUrl) && (
|
{(faviconPreviewUrl || faviconFileUrl) && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
key={faviconPreviewUrl || faviconFileUrl}
|
key={faviconPreviewUrl || faviconFileUrl}
|
||||||
src={faviconPreviewUrl || faviconFileUrl || ''}
|
fileId={faviconFileAttachmentUuid}
|
||||||
|
src={faviconPreviewUrl || faviconFileUrl}
|
||||||
alt="Favicon preview"
|
alt="Favicon preview"
|
||||||
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', width: '64px', height: '64px' }}
|
style={{ display: 'block', width: '64px', height: '64px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load favicon preview image', {
|
|
||||||
faviconFileUrl,
|
|
||||||
faviconPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -6,35 +6,42 @@ import { tenantService } from '@/services/tenant-service';
|
|||||||
import { fileService } from '@/services/file-service';
|
import { fileService } from '@/services/file-service';
|
||||||
import { showToast } from '@/utils/toast';
|
import { showToast } from '@/utils/toast';
|
||||||
import { updateTheme } from '@/store/themeSlice';
|
import { updateTheme } from '@/store/themeSlice';
|
||||||
import { PrimaryButton } from '@/components/shared';
|
import { PrimaryButton, AuthenticatedImage } from '@/components/shared';
|
||||||
import type { Tenant } from '@/types/tenant';
|
import type { Tenant } from '@/types/tenant';
|
||||||
|
|
||||||
|
// Helper function to get base URL with protocol
|
||||||
|
const getBaseUrlWithProtocol = (): string => {
|
||||||
|
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
||||||
|
};
|
||||||
|
|
||||||
const Settings = (): ReactElement => {
|
const Settings = (): ReactElement => {
|
||||||
const tenantId = useAppSelector((state) => state.auth.tenantId);
|
const tenantId = useAppSelector((state) => state.auth.tenantId);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Tenant data
|
// Tenant data
|
||||||
const [tenant, setTenant] = useState<Tenant | null>(null);
|
const [tenant, setTenant] = useState<Tenant | null>(null);
|
||||||
|
|
||||||
// Color states
|
// Color states
|
||||||
const [primaryColor, setPrimaryColor] = useState<string>('#112868');
|
const [primaryColor, setPrimaryColor] = useState<string>('#112868');
|
||||||
const [secondaryColor, setSecondaryColor] = useState<string>('#23DCE1');
|
const [secondaryColor, setSecondaryColor] = useState<string>('#23DCE1');
|
||||||
const [accentColor, setAccentColor] = useState<string>('#084CC8');
|
const [accentColor, setAccentColor] = useState<string>('#084CC8');
|
||||||
|
|
||||||
// Logo states
|
// Logo states
|
||||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||||
const [logoFilePath, setLogoFilePath] = useState<string | null>(null);
|
const [logoFilePath, setLogoFilePath] = useState<string | null>(null);
|
||||||
|
const [logoFileAttachmentUuid, setLogoFileAttachmentUuid] = useState<string | null>(null);
|
||||||
const [logoFileUrl, setLogoFileUrl] = useState<string | null>(null);
|
const [logoFileUrl, setLogoFileUrl] = useState<string | null>(null);
|
||||||
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
||||||
const [isUploadingLogo, setIsUploadingLogo] = useState<boolean>(false);
|
const [isUploadingLogo, setIsUploadingLogo] = useState<boolean>(false);
|
||||||
|
|
||||||
// Favicon states
|
// Favicon states
|
||||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||||
const [faviconFilePath, setFaviconFilePath] = useState<string | null>(null);
|
const [faviconFilePath, setFaviconFilePath] = useState<string | null>(null);
|
||||||
|
const [faviconFileAttachmentUuid, setFaviconFileAttachmentUuid] = useState<string | null>(null);
|
||||||
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
const [faviconFileUrl, setFaviconFileUrl] = useState<string | null>(null);
|
||||||
const [faviconPreviewUrl, setFaviconPreviewUrl] = useState<string | null>(null);
|
const [faviconPreviewUrl, setFaviconPreviewUrl] = useState<string | null>(null);
|
||||||
const [isUploadingFavicon, setIsUploadingFavicon] = useState<boolean>(false);
|
const [isUploadingFavicon, setIsUploadingFavicon] = useState<boolean>(false);
|
||||||
@ -54,28 +61,30 @@ const Settings = (): ReactElement => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const response = await tenantService.getById(tenantId);
|
const response = await tenantService.getById(tenantId);
|
||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
const tenantData = response.data;
|
const tenantData = response.data;
|
||||||
setTenant(tenantData);
|
setTenant(tenantData);
|
||||||
|
|
||||||
// Set colors
|
// Set colors
|
||||||
setPrimaryColor(tenantData.primary_color || '#112868');
|
setPrimaryColor(tenantData.primary_color || '#112868');
|
||||||
setSecondaryColor(tenantData.secondary_color || '#23DCE1');
|
setSecondaryColor(tenantData.secondary_color || '#23DCE1');
|
||||||
setAccentColor(tenantData.accent_color || '#084CC8');
|
setAccentColor(tenantData.accent_color || '#084CC8');
|
||||||
|
|
||||||
// Set logo
|
// Set logo
|
||||||
if (tenantData.logo_file_path) {
|
if (tenantData.logo_file_path) {
|
||||||
setLogoFileUrl(tenantData.logo_file_path);
|
setLogoFileUrl(tenantData.logo_file_path);
|
||||||
setLogoFilePath(tenantData.logo_file_path);
|
setLogoFilePath(tenantData.logo_file_path);
|
||||||
setLogoError(null); // Clear error if existing logo is found
|
setLogoFileAttachmentUuid(tenantData.logo_file_attachment_uuid || null);
|
||||||
|
setLogoError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set favicon
|
// Set favicon
|
||||||
if (tenantData.favicon_file_path) {
|
if (tenantData.favicon_file_path) {
|
||||||
setFaviconFileUrl(tenantData.favicon_file_path);
|
setFaviconFileUrl(tenantData.favicon_file_path);
|
||||||
setFaviconFilePath(tenantData.favicon_file_path);
|
setFaviconFilePath(tenantData.favicon_file_path);
|
||||||
setFaviconError(null); // Clear error if existing favicon is found
|
setFaviconFileAttachmentUuid(tenantData.favicon_file_attachment_uuid || null);
|
||||||
|
setFaviconError(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -134,10 +143,16 @@ const Settings = (): ReactElement => {
|
|||||||
setIsUploadingLogo(true);
|
setIsUploadingLogo(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const response = await fileService.upload(file, 'tenant', tenantId || undefined);
|
||||||
setLogoFilePath(response.data.file_url);
|
const fileId = response.data.id;
|
||||||
setLogoFileUrl(response.data.file_url);
|
setLogoFileAttachmentUuid(fileId);
|
||||||
setLogoError(null); // Clear error on successful upload
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setLogoFilePath(formattedUrl);
|
||||||
|
setLogoFileUrl(formattedUrl);
|
||||||
|
|
||||||
|
setLogoError(null);
|
||||||
showToast.success('Logo uploaded successfully');
|
showToast.success('Logo uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -151,6 +166,7 @@ const Settings = (): ReactElement => {
|
|||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFileUrl(null);
|
setLogoFileUrl(null);
|
||||||
setLogoFilePath(null);
|
setLogoFilePath(null);
|
||||||
|
setLogoFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingLogo(false);
|
setIsUploadingLogo(false);
|
||||||
}
|
}
|
||||||
@ -184,10 +200,16 @@ const Settings = (): ReactElement => {
|
|||||||
setIsUploadingFavicon(true);
|
setIsUploadingFavicon(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fileService.uploadSimple(file);
|
const response = await fileService.upload(file, 'tenant', tenantId || undefined);
|
||||||
setFaviconFilePath(response.data.file_url);
|
const fileId = response.data.id;
|
||||||
setFaviconFileUrl(response.data.file_url);
|
setFaviconFileAttachmentUuid(fileId);
|
||||||
setFaviconError(null); // Clear error on successful upload
|
|
||||||
|
const baseUrl = getBaseUrlWithProtocol();
|
||||||
|
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
|
||||||
|
setFaviconFilePath(formattedUrl);
|
||||||
|
setFaviconFileUrl(formattedUrl);
|
||||||
|
|
||||||
|
setFaviconError(null);
|
||||||
showToast.success('Favicon uploaded successfully');
|
showToast.success('Favicon uploaded successfully');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@ -201,6 +223,7 @@ const Settings = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
setFaviconFilePath(null);
|
setFaviconFilePath(null);
|
||||||
|
setFaviconFileAttachmentUuid(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploadingFavicon(false);
|
setIsUploadingFavicon(false);
|
||||||
}
|
}
|
||||||
@ -214,7 +237,8 @@ const Settings = (): ReactElement => {
|
|||||||
setLogoPreviewUrl(null);
|
setLogoPreviewUrl(null);
|
||||||
setLogoFileUrl(null);
|
setLogoFileUrl(null);
|
||||||
setLogoFilePath(null);
|
setLogoFilePath(null);
|
||||||
setLogoError(null); // Clear error on delete
|
setLogoFileAttachmentUuid(null);
|
||||||
|
setLogoError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('logo-upload') as HTMLInputElement;
|
const fileInput = document.getElementById('logo-upload') as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
@ -230,7 +254,8 @@ const Settings = (): ReactElement => {
|
|||||||
setFaviconPreviewUrl(null);
|
setFaviconPreviewUrl(null);
|
||||||
setFaviconFileUrl(null);
|
setFaviconFileUrl(null);
|
||||||
setFaviconFilePath(null);
|
setFaviconFilePath(null);
|
||||||
setFaviconError(null); // Clear error on delete
|
setFaviconFileAttachmentUuid(null);
|
||||||
|
setFaviconError(null);
|
||||||
// Reset the file input
|
// Reset the file input
|
||||||
const fileInput = document.getElementById('favicon-upload') as HTMLInputElement;
|
const fileInput = document.getElementById('favicon-upload') as HTMLInputElement;
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
@ -267,7 +292,7 @@ const Settings = (): ReactElement => {
|
|||||||
// Build update data matching EditTenantModal format
|
// Build update data matching EditTenantModal format
|
||||||
const existingSettings = (tenant.settings as Record<string, unknown>) || {};
|
const existingSettings = (tenant.settings as Record<string, unknown>) || {};
|
||||||
const existingContact = (existingSettings.contact as Record<string, unknown>) || {};
|
const existingContact = (existingSettings.contact as Record<string, unknown>) || {};
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tenant.name,
|
name: tenant.name,
|
||||||
slug: tenant.slug,
|
slug: tenant.slug,
|
||||||
@ -285,7 +310,9 @@ const Settings = (): ReactElement => {
|
|||||||
secondary_color: secondaryColor || undefined,
|
secondary_color: secondaryColor || undefined,
|
||||||
accent_color: accentColor || undefined,
|
accent_color: accentColor || undefined,
|
||||||
logo_file_path: logoFilePath || undefined,
|
logo_file_path: logoFilePath || undefined,
|
||||||
|
logo_file_attachment_uuid: logoFileAttachmentUuid || undefined,
|
||||||
favicon_file_path: faviconFilePath || undefined,
|
favicon_file_path: faviconFilePath || undefined,
|
||||||
|
favicon_file_attachment_uuid: faviconFileAttachmentUuid || undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -294,7 +321,7 @@ const Settings = (): ReactElement => {
|
|||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
showToast.success('Settings updated successfully');
|
showToast.success('Settings updated successfully');
|
||||||
|
|
||||||
// Update theme in Redux
|
// Update theme in Redux
|
||||||
dispatch(
|
dispatch(
|
||||||
updateTheme({
|
updateTheme({
|
||||||
@ -313,7 +340,9 @@ const Settings = (): ReactElement => {
|
|||||||
secondary_color: secondaryColor,
|
secondary_color: secondaryColor,
|
||||||
accent_color: accentColor,
|
accent_color: accentColor,
|
||||||
logo_file_path: logoFilePath,
|
logo_file_path: logoFilePath,
|
||||||
|
logo_file_attachment_uuid: logoFileAttachmentUuid,
|
||||||
favicon_file_path: faviconFilePath,
|
favicon_file_path: faviconFilePath,
|
||||||
|
favicon_file_attachment_uuid: faviconFileAttachmentUuid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -429,18 +458,12 @@ const Settings = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{(logoPreviewUrl || logoFileUrl) && (
|
{(logoPreviewUrl || logoFileUrl) && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
src={logoPreviewUrl || logoFileUrl || ''}
|
fileId={logoFileAttachmentUuid}
|
||||||
|
src={logoPreviewUrl || logoFileUrl}
|
||||||
alt="Logo preview"
|
alt="Logo preview"
|
||||||
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', maxHeight: '80px' }}
|
style={{ display: 'block', maxHeight: '80px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load logo preview image', {
|
|
||||||
logoFileUrl,
|
|
||||||
logoPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -497,18 +520,12 @@ const Settings = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{(faviconPreviewUrl || faviconFileUrl) && (
|
{(faviconPreviewUrl || faviconFileUrl) && (
|
||||||
<div className="mt-2 relative inline-block">
|
<div className="mt-2 relative inline-block">
|
||||||
<img
|
<AuthenticatedImage
|
||||||
src={faviconPreviewUrl || faviconFileUrl || ''}
|
fileId={faviconFileAttachmentUuid}
|
||||||
|
src={faviconPreviewUrl || faviconFileUrl}
|
||||||
alt="Favicon preview"
|
alt="Favicon preview"
|
||||||
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: 'block', width: '64px', height: '64px' }}
|
style={{ display: 'block', width: '64px', height: '64px' }}
|
||||||
onError={(e) => {
|
|
||||||
console.error('Failed to load favicon preview image', {
|
|
||||||
faviconFileUrl,
|
|
||||||
faviconPreviewUrl,
|
|
||||||
src: e.currentTarget.src,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -3,13 +3,40 @@ import apiClient from './api-client';
|
|||||||
export interface FileUploadResponse {
|
export interface FileUploadResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: {
|
data: {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
original_name: string;
|
original_name: string;
|
||||||
|
stored_name: string;
|
||||||
file_path: string;
|
file_path: string;
|
||||||
file_url: string;
|
|
||||||
mime_type: string;
|
mime_type: string;
|
||||||
file_size: number;
|
file_size: number;
|
||||||
|
checksum: string;
|
||||||
|
storage_provider: string;
|
||||||
|
storage_bucket: string | null;
|
||||||
|
storage_region: string | null;
|
||||||
|
entity_type: string;
|
||||||
|
entity_id: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
version: number;
|
||||||
|
is_current_version: boolean;
|
||||||
|
previous_version_id: string | null;
|
||||||
|
is_public: boolean;
|
||||||
|
access_level: string;
|
||||||
|
download_count: number;
|
||||||
|
has_thumbnail: boolean;
|
||||||
|
thumbnail_path: string | null;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
scan_status: string;
|
||||||
|
scanned_at: string | null;
|
||||||
|
uploaded_by: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
source_module: string;
|
||||||
file_size_formatted: string;
|
file_size_formatted: string;
|
||||||
uploaded_at: string;
|
file_url?: string; // Kept for backward compatibility
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,11 +44,30 @@ export const fileService = {
|
|||||||
uploadSimple: async (file: File): Promise<FileUploadResponse> => {
|
uploadSimple: async (file: File): Promise<FileUploadResponse> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
const response = await apiClient.post<FileUploadResponse>('/files/upload', formData);
|
||||||
// Axios automatically sets Content-Type to multipart/form-data with boundary for FormData
|
|
||||||
// The interceptor will still add the Authorization header
|
|
||||||
// Don't set Content-Type explicitly - let axios handle it with the correct boundary
|
|
||||||
const response = await apiClient.post<FileUploadResponse>('/files/upload/simple', formData);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upload: async (
|
||||||
|
file: File,
|
||||||
|
entityType: string = 'tenant',
|
||||||
|
entityId?: string,
|
||||||
|
tenantId?: string
|
||||||
|
): Promise<FileUploadResponse> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('entity_type', entityType);
|
||||||
|
if (entityId) formData.append('entity_id', entityId);
|
||||||
|
if (tenantId) formData.append('tenantId', tenantId);
|
||||||
|
|
||||||
|
const response = await apiClient.post<FileUploadResponse>('/files/upload', formData);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
getPreview: async (fileId: string): Promise<string> => {
|
||||||
|
const response = await apiClient.get(`/files/${fileId}/preview`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
return URL.createObjectURL(response.data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,7 +35,9 @@ export interface TenantAdmin {
|
|||||||
|
|
||||||
export interface TenantBranding {
|
export interface TenantBranding {
|
||||||
logo_file_path?: string | null;
|
logo_file_path?: string | null;
|
||||||
|
logo_file_attachment_uuid?: string | null;
|
||||||
favicon_file_path?: string | null;
|
favicon_file_path?: string | null;
|
||||||
|
favicon_file_attachment_uuid?: string | null;
|
||||||
primary_color?: string | null;
|
primary_color?: string | null;
|
||||||
secondary_color?: string | null;
|
secondary_color?: string | null;
|
||||||
accent_color?: string | null;
|
accent_color?: string | null;
|
||||||
@ -54,7 +56,9 @@ export interface Tenant {
|
|||||||
enable_sso?: boolean;
|
enable_sso?: boolean;
|
||||||
enable_2fa?: boolean;
|
enable_2fa?: boolean;
|
||||||
logo_file_path?: string | null;
|
logo_file_path?: string | null;
|
||||||
|
logo_file_attachment_uuid?: string | null;
|
||||||
favicon_file_path?: string | null;
|
favicon_file_path?: string | null;
|
||||||
|
favicon_file_attachment_uuid?: string | null;
|
||||||
primary_color?: string | null;
|
primary_color?: string | null;
|
||||||
secondary_color?: string | null;
|
secondary_color?: string | null;
|
||||||
accent_color?: string | null;
|
accent_color?: string | null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user