From a79145f0d2b6b7d7e4574e01c5457ff8d7199652 Mon Sep 17 00:00:00 2001 From: Ysterbaard Date: Wed, 19 Aug 2026 12:04:58 +0200 Subject: [PATCH] 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. --- components/DeleteConfirmModal.tsx | 6 ++ components/EditEntityModal.tsx | 118 +++++++++++++++--------------- 2 files changed, 65 insertions(+), 59 deletions(-) diff --git a/components/DeleteConfirmModal.tsx b/components/DeleteConfirmModal.tsx index f488bf9..f772449 100644 --- a/components/DeleteConfirmModal.tsx +++ b/components/DeleteConfirmModal.tsx @@ -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."); diff --git a/components/EditEntityModal.tsx b/components/EditEntityModal.tsx index 8f5d7ef..d6e4b33 100644 --- a/components/EditEntityModal.tsx +++ b/components/EditEntityModal.tsx @@ -250,6 +250,12 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter try { const fields: Record = {}; + // 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] });