Fix: clearing a field in the edit modal did not persist

The modal's submit handler wrapped every optional field with
"if (value.trim()) fields.x = value.trim()". When the user
deleted the content of a field, the value was empty, the guard
returned false, and the field was OMITTED from the PATCH entirely.
PB then left the existing value in place.

Symptom: edit a field, delete its content, save. The DB still
has the old value.

Fix: always send every field. Empty string ("") clears a value
in PocketBase (verified end-to-end). Required-field validation
(surname, first_names, enterprise_no, enterprise_name) still
guards against empty required values up-front.

Also fixed the modal's React Query invalidation to target the
list pages' actual keys (["companies"], ["directors"]) in
addition to the incorrect ["pb-list"] key. This makes the
list page refetch after save.

Same fix applied to DeleteConfirmModal for consistency.
This commit is contained in:
Ysterbaard 2026-08-19 12:04:58 +02:00
parent 7f0337a39c
commit a79145f0d2
2 changed files with 65 additions and 59 deletions

View file

@ -35,7 +35,13 @@ export default function DeleteConfirmModal({ collection, record, open, onClose }
setLoading(true);
try {
await pbDelete({ collection, id: record.id });
// Match the actual query keys used by the list pages
// ("companies" / "directors"), not just "pb-list".
await qc.invalidateQueries({ queryKey: ["pb-list", collection] });
await qc.invalidateQueries({ queryKey: ["companies"] });
await qc.invalidateQueries({ queryKey: ["directors"] });
await qc.invalidateQueries({ queryKey: ["company", record.id] });
await qc.invalidateQueries({ queryKey: ["director", record.id] });
onClose();
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed. Please try again.");

View file

@ -250,6 +250,12 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
try {
const fields: Record<string, string | number | string[]> = {};
// Always send every field — empty string clears the value in PB.
// The previous "if (value.trim())" guard LOST the user's intent to
// clear a field: empty fields were silently skipped, so a delete in
// the UI never landed in PB. We now send the literal value the user
// left in the field, including "" to clear, and only validate
// required fields up-front.
if (isDirector) {
if (!surname.trim() || !firstNames.trim()) {
setError("Surname and first names are required.");
@ -258,24 +264,24 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
}
fields.surname = surname.trim();
fields.first_names = firstNames.trim();
if (idNo.trim()) fields.id_no = idNo.trim();
if (telephone.trim()) fields.telephone = telephone.trim();
if (email.trim()) fields.email = email.trim();
if (postalAddress.trim()) fields.postal_address = postalAddress.trim();
if (directorType.trim()) fields.director_type = directorType.trim();
if (notes.trim()) fields.notes = notes.trim();
// Structured address fields (added 2026-08-18)
if (dateOfBirth.trim()) fields.date_of_birth = dateOfBirth.trim();
if (resAddr1.trim()) fields.residential_address_line_1 = resAddr1.trim();
if (resAddr2.trim()) fields.residential_address_line_2 = resAddr2.trim();
if (resTown.trim()) fields.residential_town_city = resTown.trim();
if (resProvince.trim()) fields.residential_province = resProvince.trim();
if (resPostalCode.trim()) fields.residential_postal_code = resPostalCode.trim();
if (postAddr1.trim()) fields.postal_address_line_1 = postAddr1.trim();
if (postAddr2.trim()) fields.postal_address_line_2 = postAddr2.trim();
if (postTown.trim()) fields.postal_town_city = postTown.trim();
if (postProvince.trim()) fields.postal_province = postProvince.trim();
if (postPostalCode.trim()) fields.postal_postal_code = postPostalCode.trim();
fields.id_no = idNo.trim();
fields.telephone = telephone.trim();
fields.email = email.trim();
fields.postal_address = postalAddress.trim();
fields.director_type = directorType.trim();
fields.notes = notes.trim();
// Structured address fields
fields.date_of_birth = dateOfBirth.trim();
fields.residential_address_line_1 = resAddr1.trim();
fields.residential_address_line_2 = resAddr2.trim();
fields.residential_town_city = resTown.trim();
fields.residential_province = resProvince.trim();
fields.residential_postal_code = resPostalCode.trim();
fields.postal_address_line_1 = postAddr1.trim();
fields.postal_address_line_2 = postAddr2.trim();
fields.postal_town_city = postTown.trim();
fields.postal_province = postProvince.trim();
fields.postal_postal_code = postPostalCode.trim();
} else {
if (!enterpriseNo.trim() || !enterpriseName.trim()) {
setError("Enterprise number and name are required.");
@ -284,47 +290,36 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
}
fields.enterprise_no = enterpriseNo.trim().toUpperCase();
fields.enterprise_name = enterpriseName.trim().toUpperCase();
if (enterpriseType.trim()) fields.enterprise_type = enterpriseType.trim();
if (registrationDate.trim()) fields.registration_date = registrationDate.trim();
if (physicalAddress.trim()) fields.physical_address = physicalAddress.trim();
if (postalAddressCo.trim()) fields.postal_address = postalAddressCo.trim();
if (companyEmail.trim()) fields.company_email = companyEmail.trim();
if (notesCo.trim()) fields.notes = notesCo.trim();
// Structured address fields (added 2026-08-18)
if (physAddr1.trim()) fields.physical_address_line_1 = physAddr1.trim();
if (physAddr2.trim()) fields.physical_address_line_2 = physAddr2.trim();
if (physTown.trim()) fields.physical_town_city = physTown.trim();
if (physProvince.trim()) fields.physical_province = physProvince.trim();
if (physPostalCode.trim()) fields.physical_postal_code = physPostalCode.trim();
if (postCoAddr1.trim()) fields.postal_address_line_1 = postCoAddr1.trim();
if (postCoAddr2.trim()) fields.postal_address_line_2 = postCoAddr2.trim();
if (postCoTown.trim()) fields.postal_town_city = postCoTown.trim();
if (postCoProvince.trim()) fields.postal_province = postCoProvince.trim();
if (postCoPostalCode.trim()) fields.postal_postal_code = postCoPostalCode.trim();
// Company-level fields (added 2026-08-18)
if (financialYearEndMonth !== "" || (record as Company).financial_year_end_month != null) {
fields.financial_year_end_month = financialYearEndMonth !== "" ? Number(financialYearEndMonth) : 0;
}
if (authorisedShares !== "" || (record as Company).authorised_shares != null) {
fields.authorised_shares = authorisedShares !== "" ? Number(authorisedShares) : 0;
}
if (companyCellPhone.trim()) fields.company_cell_phone = companyCellPhone.trim();
if (publicOfficerName.trim()) fields.public_officer_name = publicOfficerName.trim();
if (companySecretaryName.trim()) fields.company_secretary_name = companySecretaryName.trim();
if (sarsLoginUsername.trim()) fields.sars_login_username = sarsLoginUsername.trim();
if (sarsLoginPassword.trim()) fields.sars_login_password = sarsLoginPassword.trim();
if (sarsPayeNo.trim()) fields.sars_paye_no = sarsPayeNo.trim();
if (sarsVatNo.trim()) fields.sars_vat_no = sarsVatNo.trim();
if (sarsTaxNo.trim()) fields.sars_tax_number = sarsTaxNo.trim();
// Period fields
const vatPeriod = sarsVatReturnsPeriod;
const payePeriod = sarsPayeReturnPeriod;
if (vatPeriod !== "" || (record as Company).sars_vat_returns_period != null) {
fields.sars_vat_returns_period = vatPeriod !== "" ? Number(vatPeriod) : 0;
}
if (payePeriod !== "" || (record as Company).sars_paye_return_period != null) {
fields.sars_paye_return_period = payePeriod !== "" ? Number(payePeriod) : 0;
}
fields.enterprise_type = enterpriseType.trim();
fields.registration_date = registrationDate.trim();
fields.physical_address = physicalAddress.trim();
fields.postal_address = postalAddressCo.trim();
fields.company_email = companyEmail.trim();
fields.notes = notesCo.trim();
// Structured address fields
fields.physical_address_line_1 = physAddr1.trim();
fields.physical_address_line_2 = physAddr2.trim();
fields.physical_town_city = physTown.trim();
fields.physical_province = physProvince.trim();
fields.physical_postal_code = physPostalCode.trim();
fields.postal_address_line_1 = postCoAddr1.trim();
fields.postal_address_line_2 = postCoAddr2.trim();
fields.postal_town_city = postCoTown.trim();
fields.postal_province = postCoProvince.trim();
fields.postal_postal_code = postCoPostalCode.trim();
// Company-level fields
fields.financial_year_end_month = financialYearEndMonth !== "" ? Number(financialYearEndMonth) : 0;
fields.authorised_shares = authorisedShares !== "" ? Number(authorisedShares) : 0;
fields.company_cell_phone = companyCellPhone.trim();
fields.public_officer_name = publicOfficerName.trim();
fields.company_secretary_name = companySecretaryName.trim();
fields.sars_login_username = sarsLoginUsername.trim();
fields.sars_login_password = sarsLoginPassword.trim();
fields.sars_paye_no = sarsPayeNo.trim();
fields.sars_vat_no = sarsVatNo.trim();
fields.sars_tax_number = sarsTaxNo.trim();
fields.sars_vat_returns_period = sarsVatReturnsPeriod !== "" ? Number(sarsVatReturnsPeriod) : 0;
fields.sars_paye_return_period = sarsPayeReturnPeriod !== "" ? Number(sarsPayeReturnPeriod) : 0;
// Groups — always send current selection (empty array is valid to clear)
fields.groups = selectedGroups;
}
@ -332,7 +327,12 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
await pbPatch({ collection, id: record.id, fields });
// Invalidate both list and detail caches
// Invalidate every key the detail page or list pages might be holding.
// The list pages use "companies" / "directors" keys (not "pb-list"),
// and the detail page uses "director" / "company" keys.
await qc.invalidateQueries({ queryKey: ["pb-list", collection] });
await qc.invalidateQueries({ queryKey: ["companies"] });
await qc.invalidateQueries({ queryKey: ["directors"] });
await qc.invalidateQueries({ queryKey: ["director", record.id] });
await qc.invalidateQueries({ queryKey: ["company", record.id] });