fix: Normalize optional user fields (department, designation, and supplier) in user modals and update AuthenticatedImage formatting.
This commit is contained in:
parent
d242bbf708
commit
c9503c78be
@ -1,104 +1,120 @@
|
|||||||
import { useState, useEffect, type ImgHTMLAttributes, type ReactElement } from 'react';
|
import {
|
||||||
import { fileService } from '@/services/file-service';
|
useState,
|
||||||
import apiClient from '@/services/api-client';
|
useEffect,
|
||||||
import { Loader2, ImageIcon } from 'lucide-react';
|
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'> {
|
interface AuthenticatedImageProps extends Omit<
|
||||||
fileId?: string | null;
|
ImgHTMLAttributes<HTMLImageElement>,
|
||||||
src?: string | null;
|
"src"
|
||||||
fallback?: ReactElement;
|
> {
|
||||||
|
fileId?: string | null;
|
||||||
|
src?: string | null;
|
||||||
|
fallback?: ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthenticatedImage = ({
|
export const AuthenticatedImage = ({
|
||||||
fileId,
|
fileId,
|
||||||
src,
|
src,
|
||||||
fallback,
|
fallback,
|
||||||
className,
|
className,
|
||||||
alt = 'Image',
|
alt = "Image",
|
||||||
...props
|
...props
|
||||||
}: AuthenticatedImageProps): ReactElement => {
|
}: AuthenticatedImageProps): ReactElement => {
|
||||||
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If it's already a blob URL (local preview) or a data URL, use it directly
|
// If it's already a blob URL (local preview) or a data URL, use it directly
|
||||||
if (src && (src.startsWith('blob:') || src.startsWith('data:'))) {
|
if (src && (src.startsWith("blob:") || src.startsWith("data:"))) {
|
||||||
setBlobUrl(src);
|
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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setBlobUrl(url);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch authenticated image:", err);
|
||||||
|
if (isMounted) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
|
fetchImage();
|
||||||
const isBackendUrl = src && src.includes(`${baseUrl}/files/`) && src.includes('/preview');
|
|
||||||
|
|
||||||
// If we have a fileId or a backend URL, fetch it via authenticated request
|
return () => {
|
||||||
if (fileId || isBackendUrl) {
|
isMounted = false;
|
||||||
let isMounted = true;
|
if (blobUrl && blobUrl.startsWith("blob:")) {
|
||||||
const fetchImage = async () => {
|
URL.revokeObjectURL(blobUrl);
|
||||||
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]);
|
};
|
||||||
|
} else if (src) {
|
||||||
if (isLoading) {
|
// For other external URLs, use them directly
|
||||||
return (
|
setBlobUrl(src);
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, [fileId, src]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<img
|
<div
|
||||||
src={blobUrl || src || ''}
|
className={`flex items-center justify-center bg-[#f5f7fa] rounded-md ${className}`}
|
||||||
className={className}
|
>
|
||||||
alt={alt}
|
<Loader2 className="w-5 h-5 text-[#6b7280] animate-spin" />
|
||||||
{...props}
|
</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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -345,6 +345,17 @@ export const EditUserModal = ({
|
|||||||
if (defaultTenantId) {
|
if (defaultTenantId) {
|
||||||
data.tenant_id = defaultTenantId;
|
data.tenant_id = defaultTenantId;
|
||||||
}
|
}
|
||||||
|
// Normalize empty strings to null for optional UUID fields
|
||||||
|
if (!data.department_id) data.department_id = undefined;
|
||||||
|
if (!data.designation_id) data.designation_id = undefined;
|
||||||
|
|
||||||
|
// If category is tenant_user, supplier_id should always be null
|
||||||
|
if (data.category === "tenant_user") {
|
||||||
|
data.supplier_id = null;
|
||||||
|
} else if (!data.supplier_id) {
|
||||||
|
data.supplier_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
await onSubmit(userId, data);
|
await onSubmit(userId, data);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (
|
if (
|
||||||
@ -491,13 +502,13 @@ export const EditUserModal = ({
|
|||||||
{ value: "supplier_user", label: "Supplier User" },
|
{ value: "supplier_user", label: "Supplier User" },
|
||||||
]}
|
]}
|
||||||
value={categoryValue || "tenant_user"}
|
value={categoryValue || "tenant_user"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => {
|
||||||
setValue(
|
const category = value as "tenant_user" | "supplier_user";
|
||||||
"category",
|
setValue("category", category, { shouldValidate: true });
|
||||||
value as "tenant_user" | "supplier_user",
|
if (category === "tenant_user") {
|
||||||
{ shouldValidate: true },
|
setValue("supplier_id", null);
|
||||||
)
|
}
|
||||||
}
|
}}
|
||||||
error={errors.category?.message}
|
error={errors.category?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -216,6 +216,17 @@ export const NewUserModal = ({
|
|||||||
clearErrors();
|
clearErrors();
|
||||||
try {
|
try {
|
||||||
const { confirmPassword, ...submitData } = data;
|
const { confirmPassword, ...submitData } = data;
|
||||||
|
// Normalize empty strings to null for optional UUID fields
|
||||||
|
if (!submitData.department_id) submitData.department_id = undefined;
|
||||||
|
if (!submitData.designation_id) submitData.designation_id = undefined;
|
||||||
|
|
||||||
|
// If category is tenant_user, supplier_id should always be null
|
||||||
|
if (submitData.category === "tenant_user") {
|
||||||
|
submitData.supplier_id = null;
|
||||||
|
} else if (!submitData.supplier_id) {
|
||||||
|
submitData.supplier_id = null;
|
||||||
|
}
|
||||||
|
|
||||||
await onSubmit(submitData);
|
await onSubmit(submitData);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Handle validation errors from API
|
// Handle validation errors from API
|
||||||
@ -387,11 +398,13 @@ export const NewUserModal = ({
|
|||||||
{ value: "supplier_user", label: "Supplier User" },
|
{ value: "supplier_user", label: "Supplier User" },
|
||||||
]}
|
]}
|
||||||
value={categoryValue || "tenant_user"}
|
value={categoryValue || "tenant_user"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => {
|
||||||
setValue("category", value as "tenant_user" | "supplier_user", {
|
const category = value as "tenant_user" | "supplier_user";
|
||||||
shouldValidate: true,
|
setValue("category", category, { shouldValidate: true });
|
||||||
})
|
if (category === "tenant_user") {
|
||||||
}
|
setValue("supplier_id", null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
error={errors.category?.message}
|
error={errors.category?.message}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user