Fix company document removal in edit mode
continuous-integration/drone/push Build is passing

This commit is contained in:
AmirReza Jamali
2026-07-07 10:54:51 +03:30
parent 98fac2c867
commit 08938ad7f0
6 changed files with 116 additions and 39 deletions
@@ -21,6 +21,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
control, control,
watch, watch,
setValue, setValue,
persistDocumentFileIds,
companyCategories, companyCategories,
} = useCreateCompanyPresenter({ editMode, defaultValues, id }); } = useCreateCompanyPresenter({ editMode, defaultValues, id });
@@ -38,6 +39,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
categoryItems, categoryItems,
watch, watch,
setValue, setValue,
persistDocumentFileIds,
})} })}
register={register} register={register}
control={control} control={control}
@@ -25,6 +25,7 @@ interface CompanyFormSectionsOptions {
categoryItems: ILabelValue[]; categoryItems: ILabelValue[];
watch: UseFormWatch<ICreateCompanyCredentials>; watch: UseFormWatch<ICreateCompanyCredentials>;
setValue: UseFormSetValue<ICreateCompanyCredentials>; setValue: UseFormSetValue<ICreateCompanyCredentials>;
persistDocumentFileIds?: (ids: string[]) => Promise<void>;
} }
const optionalNumber = (value: unknown) => { const optionalNumber = (value: unknown) => {
@@ -240,6 +241,7 @@ export const createCompanyFormSections = ({
categoryItems, categoryItems,
watch, watch,
setValue, setValue,
persistDocumentFileIds,
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [ }: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
{ {
id: companyInformationSectionId, id: companyInformationSectionId,
@@ -652,7 +654,14 @@ export const createCompanyFormSections = ({
<CompanyDocuments <CompanyDocuments
companyId={editMode ? id : undefined} companyId={editMode ? id : undefined}
value={watch("document_file_ids") ?? []} value={watch("document_file_ids") ?? []}
onChange={(ids) => setValue("document_file_ids", ids)} onChange={(ids) =>
setValue("document_file_ids", ids, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
})
}
onRemovePersist={persistDocumentFileIds}
/> />
), ),
}, },
@@ -8,9 +8,15 @@ interface IProps {
companyId?: string; companyId?: string;
value: string[]; value: string[];
onChange: (ids: string[]) => void; onChange: (ids: string[]) => void;
onRemovePersist?: (ids: string[]) => Promise<void>;
} }
const CompanyDocuments = ({ companyId, value, onChange }: IProps) => { const CompanyDocuments = ({
companyId,
value,
onChange,
onRemovePersist,
}: IProps) => {
const { const {
isUploading, isUploading,
isBusy, isBusy,
@@ -20,13 +26,18 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
errorMessage, errorMessage,
onSelectFiles, onSelectFiles,
onRemove, onRemove,
} = useCompanyDocumentsPresenter({ companyId, value, onChange }); } = useCompanyDocumentsPresenter({
companyId,
value,
onChange,
onRemovePersist,
});
const inputId = "company-documents-input"; const inputId = "company-documents-input";
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="relative overflow-hidden rounded-2xl border border-stroke/70 bg-gradient-to-br from-white via-white to-primary/[0.04] p-4 shadow-theme-xs dark:border-dark-3 dark:from-dark-2 dark:via-dark-2 dark:to-primary/[0.12]"> <div className="shadow-theme-xs relative overflow-hidden rounded-2xl border border-stroke/70 bg-gradient-to-br from-white via-white to-primary/[0.04] p-4 dark:border-dark-3 dark:from-dark-2 dark:via-dark-2 dark:to-primary/[0.12]">
<input <input
id={inputId} id={inputId}
type="file" type="file"
@@ -45,8 +56,13 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
: "cursor-pointer hover:-translate-y-0.5 hover:border-primary hover:bg-primary/[0.03] dark:hover:bg-primary/[0.08]" : "cursor-pointer hover:-translate-y-0.5 hover:border-primary hover:bg-primary/[0.03] dark:hover:bg-primary/[0.08]"
}`} }`}
> >
<div className="mb-3 rounded-2xl bg-primary/[0.12] p-3 text-primary shadow-theme-xs transition-transform duration-300 group-hover:scale-105"> <div className="shadow-theme-xs mb-3 rounded-2xl bg-primary/[0.12] p-3 text-primary transition-transform duration-300 group-hover:scale-105">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
@@ -80,7 +96,9 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
</div> </div>
) : removingFileId ? ( ) : removingFileId ? (
<div className="mt-3"> <div className="mt-3">
<p className="text-xs font-medium text-primary">Removing document</p> <p className="text-xs font-medium text-primary">
Removing document
</p>
<div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3"> <div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-gray-2 dark:bg-dark-3">
<div className="h-full w-full animate-pulse rounded-full bg-gradient-to-r from-primary to-purple-500" /> <div className="h-full w-full animate-pulse rounded-full bg-gradient-to-r from-primary to-purple-500" />
</div> </div>
@@ -115,8 +133,12 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
</p> </p>
<ul className="space-y-1"> <ul className="space-y-1">
{failed.map((item, index) => ( {failed.map((item, index) => (
<li key={`${item.filename}-${index}`} className="text-sm text-error"> <li
<span className="font-medium">{item.filename}</span>: {item.error} key={`${item.filename}-${index}`}
className="text-sm text-error"
>
<span className="font-medium">{item.filename}</span>:{" "}
{item.error}
</li> </li>
))} ))}
</ul> </ul>
@@ -1,10 +1,7 @@
"use client"; "use client";
import { useUploadCompanyDocuments } from "@/hooks/queries"; import { useUploadCompanyDocuments } from "@/hooks/queries";
import { fileService, FailedFile } from "@/lib/api"; import { fileService, FailedFile } from "@/lib/api";
import { import { MAX_DOCUMENT_FILES, validateDocumentFile } from "@/utils/shared";
MAX_DOCUMENT_FILES,
validateDocumentFile,
} from "@/utils/shared";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
interface IProps { interface IProps {
@@ -14,6 +11,8 @@ interface IProps {
value: string[]; value: string[];
/** Persists the updated document file id list back to the form. */ /** Persists the updated document file id list back to the form. */
onChange: (ids: string[]) => void; onChange: (ids: string[]) => void;
/** Persists document removal for an existing company. */
onRemovePersist?: (ids: string[]) => Promise<void>;
} }
/** /**
@@ -28,6 +27,7 @@ const useCompanyDocumentsPresenter = ({
companyId, companyId,
value, value,
onChange, onChange,
onRemovePersist,
}: IProps) => { }: IProps) => {
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [removingFileId, setRemovingFileId] = useState<string | null>(null); const [removingFileId, setRemovingFileId] = useState<string | null>(null);
@@ -106,15 +106,18 @@ const useCompanyDocumentsPresenter = ({
setErrorMessage(undefined); setErrorMessage(undefined);
try { try {
const nextValue = value.filter((id) => id !== fileId);
// In create mode the file is an orphan in storage (not linked to any // In create mode the file is an orphan in storage (not linked to any
// company), so clean it up before dropping it from the form. In edit mode // company), so clean it up before dropping it from the form. In edit mode
// the removal is persisted by the form's update (PUT replaces the full // persist the replacement document list immediately.
// document list).
if (!companyId) { if (!companyId) {
await fileService.deleteFile(fileId); await fileService.deleteFile(fileId);
} else {
await onRemovePersist?.(nextValue);
} }
onChange(value.filter((id) => id !== fileId)); onChange(nextValue);
} catch (error) { } catch (error) {
console.error("ERROR caught while deleting company document:", error); console.error("ERROR caught while deleting company document:", error);
setErrorMessage("Failed to remove document. Please try again."); setErrorMessage("Failed to remove document. Please try again.");
@@ -49,6 +49,13 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany( const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
id as string, id as string,
); );
const { mutateAsync: updateCompanyDocuments } = useUpdateCompany(
id as string,
{
redirectOnSuccess: false,
showSuccessToast: false,
},
);
const optionalNumber = (value: unknown) => { const optionalNumber = (value: unknown) => {
if (value === "" || value === null || value === undefined) return undefined; if (value === "" || value === null || value === undefined) return undefined;
@@ -56,27 +63,30 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
return Number(value); return Number(value);
}; };
const formatCompanyData = (
data: ICreateCompanyCredentials,
documentFileIds = data.document_file_ids,
) => ({
...data,
founded_year: optionalNumber(data.founded_year),
employee_count: optionalNumber(data.employee_count),
annual_revenue: optionalNumber(data.annual_revenue),
document_file_ids: documentFileIds ?? [],
tags: {
keywords: data?.tags?.keywords?.length ? data?.tags?.keywords : [],
categories: data?.tags?.categories?.length ? data?.tags?.categories : [],
cpv_codes: data?.tags?.cpv_codes?.length ? data?.tags?.cpv_codes : [],
specializations: data?.tags?.specializations?.length
? data?.tags?.specializations
: [],
certifications: data?.tags?.certifications?.length
? data?.tags?.certifications
: [],
},
});
const onSubmit: SubmitHandler<ICreateCompanyCredentials> = (data) => { const onSubmit: SubmitHandler<ICreateCompanyCredentials> = (data) => {
const formattedData = { const formattedData = formatCompanyData(data);
...data,
founded_year: optionalNumber(data.founded_year),
employee_count: optionalNumber(data.employee_count),
annual_revenue: optionalNumber(data.annual_revenue),
document_file_ids: data.document_file_ids ?? [],
tags: {
keywords: data?.tags?.keywords?.length ? data?.tags?.keywords : [],
categories: data?.tags?.categories?.length
? data?.tags?.categories
: [],
cpv_codes: data?.tags?.cpv_codes?.length ? data?.tags?.cpv_codes : [],
specializations: data?.tags?.specializations?.length
? data?.tags?.specializations
: [],
certifications: data?.tags?.certifications?.length
? data?.tags?.certifications
: [],
},
};
if (editMode) { if (editMode) {
updateCompany({ updateCompany({
id: id as string, id: id as string,
@@ -87,6 +97,18 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
} }
}; };
const persistDocumentFileIds = async (documentFileIds: string[]) => {
if (!editMode || !id) return;
await updateCompanyDocuments({
id,
credentials: formatCompanyData(
watch() as ICreateCompanyCredentials,
documentFileIds,
) as ICreateCompanyCredentials,
});
};
const handleCancel = () => { const handleCancel = () => {
reset(); reset();
router.back(); router.back();
@@ -104,6 +126,7 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
router, router,
watch, watch,
setValue, setValue,
persistDocumentFileIds,
companyCategories, companyCategories,
}; };
}; };
+21 -3
View File
@@ -31,7 +31,18 @@ export const useCreateCompany = () => {
}); });
}; };
export const useUpdateCompany = (id: string) => { interface UseUpdateCompanyOptions {
redirectOnSuccess?: boolean;
showSuccessToast?: boolean;
}
export const useUpdateCompany = (
id: string,
{
redirectOnSuccess = true,
showSuccessToast = true,
}: UseUpdateCompanyOptions = {},
) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const mutationKey = useMemo( const mutationKey = useMemo(
@@ -42,14 +53,21 @@ export const useUpdateCompany = (id: string) => {
mutationKey, mutationKey,
mutationFn: companiesService.updateCompany, mutationFn: companiesService.updateCompany,
onSuccess: () => { onSuccess: () => {
toast.success("Company updated successfully"); if (showSuccessToast) {
toast.success("Company updated successfully");
}
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL], queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
}); });
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"], queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
}); });
router.push("/companies"); queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, id, "details"],
});
if (redirectOnSuccess) {
router.push("/companies");
}
}, },
}); });
}; };