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,
watch,
setValue,
persistDocumentFileIds,
companyCategories,
} = useCreateCompanyPresenter({ editMode, defaultValues, id });
@@ -38,6 +39,7 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
categoryItems,
watch,
setValue,
persistDocumentFileIds,
})}
register={register}
control={control}
@@ -25,6 +25,7 @@ interface CompanyFormSectionsOptions {
categoryItems: ILabelValue[];
watch: UseFormWatch<ICreateCompanyCredentials>;
setValue: UseFormSetValue<ICreateCompanyCredentials>;
persistDocumentFileIds?: (ids: string[]) => Promise<void>;
}
const optionalNumber = (value: unknown) => {
@@ -240,6 +241,7 @@ export const createCompanyFormSections = ({
categoryItems,
watch,
setValue,
persistDocumentFileIds,
}: CompanyFormSectionsOptions): DynamicFormSection<ICreateCompanyCredentials>[] => [
{
id: companyInformationSectionId,
@@ -652,7 +654,14 @@ export const createCompanyFormSections = ({
<CompanyDocuments
companyId={editMode ? id : undefined}
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;
value: string[];
onChange: (ids: string[]) => void;
onRemovePersist?: (ids: string[]) => Promise<void>;
}
const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
const CompanyDocuments = ({
companyId,
value,
onChange,
onRemovePersist,
}: IProps) => {
const {
isUploading,
isBusy,
@@ -20,13 +26,18 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
errorMessage,
onSelectFiles,
onRemove,
} = useCompanyDocumentsPresenter({ companyId, value, onChange });
} = useCompanyDocumentsPresenter({
companyId,
value,
onChange,
onRemovePersist,
});
const inputId = "company-documents-input";
return (
<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
id={inputId}
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]"
}`}
>
<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">
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -80,7 +96,9 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
</div>
) : removingFileId ? (
<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="h-full w-full animate-pulse rounded-full bg-gradient-to-r from-primary to-purple-500" />
</div>
@@ -115,8 +133,12 @@ const CompanyDocuments = ({ companyId, value, onChange }: IProps) => {
</p>
<ul className="space-y-1">
{failed.map((item, index) => (
<li key={`${item.filename}-${index}`} className="text-sm text-error">
<span className="font-medium">{item.filename}</span>: {item.error}
<li
key={`${item.filename}-${index}`}
className="text-sm text-error"
>
<span className="font-medium">{item.filename}</span>:{" "}
{item.error}
</li>
))}
</ul>
@@ -1,10 +1,7 @@
"use client";
import { useUploadCompanyDocuments } from "@/hooks/queries";
import { fileService, FailedFile } from "@/lib/api";
import {
MAX_DOCUMENT_FILES,
validateDocumentFile,
} from "@/utils/shared";
import { MAX_DOCUMENT_FILES, validateDocumentFile } from "@/utils/shared";
import { ChangeEvent, useState } from "react";
interface IProps {
@@ -14,6 +11,8 @@ interface IProps {
value: string[];
/** Persists the updated document file id list back to the form. */
onChange: (ids: string[]) => void;
/** Persists document removal for an existing company. */
onRemovePersist?: (ids: string[]) => Promise<void>;
}
/**
@@ -28,6 +27,7 @@ const useCompanyDocumentsPresenter = ({
companyId,
value,
onChange,
onRemovePersist,
}: IProps) => {
const [isUploading, setIsUploading] = useState(false);
const [removingFileId, setRemovingFileId] = useState<string | null>(null);
@@ -106,15 +106,18 @@ const useCompanyDocumentsPresenter = ({
setErrorMessage(undefined);
try {
const nextValue = value.filter((id) => id !== fileId);
// 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
// the removal is persisted by the form's update (PUT replaces the full
// document list).
// persist the replacement document list immediately.
if (!companyId) {
await fileService.deleteFile(fileId);
} else {
await onRemovePersist?.(nextValue);
}
onChange(value.filter((id) => id !== fileId));
onChange(nextValue);
} catch (error) {
console.error("ERROR caught while deleting company document:", error);
setErrorMessage("Failed to remove document. Please try again.");
@@ -49,6 +49,13 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
const { mutate: updateCompany, isPending: isUpdating } = useUpdateCompany(
id as string,
);
const { mutateAsync: updateCompanyDocuments } = useUpdateCompany(
id as string,
{
redirectOnSuccess: false,
showSuccessToast: false,
},
);
const optionalNumber = (value: unknown) => {
if (value === "" || value === null || value === undefined) return undefined;
@@ -56,27 +63,30 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
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 formattedData = {
...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
: [],
},
};
const formattedData = formatCompanyData(data);
if (editMode) {
updateCompany({
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 = () => {
reset();
router.back();
@@ -104,6 +126,7 @@ const useCreateCompanyPresenter = ({ defaultValues, editMode, id }: IProps) => {
router,
watch,
setValue,
persistDocumentFileIds,
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 router = useRouter();
const mutationKey = useMemo(
@@ -42,14 +53,21 @@ export const useUpdateCompany = (id: string) => {
mutationKey,
mutationFn: companiesService.updateCompany,
onSuccess: () => {
toast.success("Company updated successfully");
if (showSuccessToast) {
toast.success("Company updated successfully");
}
queryClient.invalidateQueries({
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
});
queryClient.invalidateQueries({
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");
}
},
});
};