Skip to content

Move page header to the same component as the page sidebar#273

Closed
BoraIneviNMI wants to merge 14 commits into
developmentfrom
FDM231-header
Closed

Move page header to the same component as the page sidebar#273
BoraIneviNMI wants to merge 14 commits into
developmentfrom
FDM231-header

Conversation

@BoraIneviNMI
Copy link
Copy Markdown
Collaborator

@BoraIneviNMI BoraIneviNMI commented Sep 22, 2025

Enhancements

  • Page header is now a single component that updates itself based on the URL.
  • A lot of route component code and loader code have been removed.
  • This also lets the user recover from failure since farm options, field options etc. can still be loaded even if the main route fails to load due to server error.
  • Error boundaries are also localized now.

Summary by CodeRabbit

  • New Features

    • Dynamic page header that adapts to the current section automatically.
    • Route-level error boundaries provide clearer, contextual error pages.
    • Fertilizer list endpoint powers dropdowns across fertilizer pages.
  • UX/Style

    • Error pages redesigned with compact cards, expandable details, and actionable buttons (reload, copy, home).
    • Subtle animation added to enhance error visuals.
    • Updated navigation: fertilizers path now uses plural.
  • Refactor

    • Streamlined layouts by removing redundant per-page headers.
  • Chores

    • Minor version bump of the app package.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Sep 22, 2025

🦋 Changeset detected

Latest commit: 7a08934

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@svenvw/fdm-app Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 22, 2025

Walkthrough

Introduces HeaderAutomatic and farm/field options store; removes inline header compositions across many routes; centralizes error rendering via InlineErrorBoundary and replaces throw-based error handling with return-based patterns; revises several loaders to drop farm/fertilizer option payloads; adds fertilizers options loader route; updates ErrorBlock UI; minor type exports and Tailwind animation.

Changes

Cohort / File(s) Summary
Header automation
fdm-app/app/components/blocks/header/automatic.tsx
New HeaderAutomatic component that selects and renders header variants based on route id and merged loader/store data.
Header components adjustments
fdm-app/app/components/blocks/header/create-farm.tsx, fdm-app/app/components/blocks/header/farm.tsx, fdm-app/app/components/blocks/header/field.tsx, fdm-app/app/components/blocks/header/fertilizer.tsx
Adds store usage for farm options, exports types (HeaderFarmOption, HeaderFieldOption), makes fertilizerOptions prop optional, updates breadcrumb path to /fertilizers, guards renders for undefined options.
Centralized error UI and boundary
fdm-app/app/components/custom/error.tsx, fdm-app/app/components/custom/inline-error-boundary.tsx, fdm-app/app/root.tsx
Overhauls ErrorBlock to Card-based UI with optional actions; adds InlineErrorBoundary handling route and generic errors; root ErrorBoundary now delegates to InlineErrorBoundary.
Header removal from routes (moved to automatic/global)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx, ...balance.nitrogen.tsx, ...field.$b_id.tsx, ...field._index.tsx, ...field.new.tsx, ...norms.tsx, ...nutrient_advice.tsx, farm.$b_id_farm.fertilizers.$p_id.tsx, farm.$b_id_farm.fertilizers._index.tsx, farm.$b_id_farm.fertilizers.new.$p_id.tsx, farm.$b_id_farm.fertilizers.new.custom.tsx, farm.$b_id_farm.fertilizers.new.tsx, farm.create.$b_id_farm.$calendar.atlas.tsx, farm.create.*.cultivations.tsx, farm.create.*.fields.tsx, farm.create._index.tsx, farm._index.tsx
Removes Header/HeaderFarm/HeaderField/HeaderFertilizer/HeaderFarmCreate usage from pages; layout simplified; some loaders no longer provide header-related data.
Loader payload reductions (drop farmOptions and related farm fields)
...balance.nitrogen.tsx, ...field._index.tsx, ...field.new.tsx, ...norms.tsx, ...nutrient_advice.tsx, farm.$b_id_farm.fertilizers.$p_id.tsx, farm.$b_id_farm.fertilizers._index.tsx, farm.$b_id_farm.fertilizers.new.$p_id.tsx, farm.$b_id_farm.fertilizers.new.custom.tsx, farm.create.$b_id_farm.$calendar.atlas.tsx, farm.create.$b_id_farm.$calendar.upload.tsx, farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx, farm.create.$b_id_farm.$calendar.cultivations.tsx, farm.create.$b_id_farm.$calendar._index.tsx
Loaders stop returning farm/farmOptions/fertilizerOptions/b_name_farm in many routes; focus on essential data (e.g., fieldOptions).
New fertilizers options loader route
fdm-app/app/routes/farm.$b_id_farm.fertilizers.tsx
Adds loader returning { fertilizerOptions } built from getFertilizers; validates params; errors via handleLoaderError.
App-level header integration and farm options enrichment
fdm-app/app/routes/farm.tsx
Loader now adds farmOptions; layout renders HeaderAutomatic inside SidebarInset.
Error handling: throw → return standardization
fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.atlas.tsx, ...harvest.$b_id_harvesting.tsx, ...harvest.new.tsx, ...$b_lu.tsx, ...cultivation.tsx, ...fertilizer.tsx, ...soil.*.tsx, farm.$b_id_farm.fertilizers.$p_id.tsx, farm.$b_id_farm.fertilizers.new.*.tsx, farm.$b_id_farm.settings*.tsx, farm.$b_id_farm.tsx, logout.tsx, organization.$slug.tsx, organization.invitations.tsx, organization.new.tsx, farm.create.*
Changes catch blocks to return handleActionError(...) (and some loaders to process handleActionError with redirects) instead of throwing; adjusts control flow for errors.
Route-level ErrorBoundaries
fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.tsx, ...nutrient_advice.$b_id.tsx, farm.$b_id_farm.tsx, farm._index.tsx, farm.create.$b_id_farm.$calendar.access.tsx, farm.create.$b_id_farm.$calendar.cultivations.tsx, farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx, farm.create.$b_id_farm.$calendar.fields.tsx
Adds exported ErrorBoundary wrappers delegating to InlineErrorBoundary; some use farm/field options store for contextual titles.
Farm/field options client store
fdm-app/app/store/farm-field-options.ts
Adds zustand store for farmOptions and fieldOptions with setters, upsert, and lookup helpers; exported hook useFarmFieldOptionsStore.
Tailwind utility
fdm-app/app/tailwind.css
Adds tractor-drive keyframes and .animate-tractor-drive utility.
Misc signature tweaks
fdm-app/app/routes/farm.$b_id_farm.tsx, farm._index.tsx, app/root.tsx, header type exports
Minor function signature updates (loader arg lists, ErrorBoundary props), type exports made public.
Changeset
.changeset/tiny-words-drum.md
Bumps @svenvw/fdm-app minor; notes error block design change.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Router as Router (Remix)
  participant Loader as Route Loader(s)
  participant Store as FarmFieldOptionsStore
  participant Header as HeaderAutomatic
  participant UI as Page

  User->>Router: Navigate to /farm/... route
  Router->>Loader: Run loaders for matched routes
  Loader-->>Router: Return data (e.g., fieldOptions, fertilizerOptions)
  Router->>Store: Optionally set farm/field options (client-side effects)
  Router->>Header: Render with route id + merged data
  Header-->>UI: Render appropriate header variant
  UI-->>User: Page content
Loading
sequenceDiagram
  autonumber
  participant Action as Action/Loader
  participant Handler as handleActionError
  participant Boundary as InlineErrorBoundary
  participant Sentry as Sentry

  Action->>Action: try { ... } catch (e)
  Action->>Handler: return handleActionError(e)
  note right of Action: Return-based error flow
  Boundary->>Boundary: Inspect error/status
  alt 401 RouteErrorResponse
    Boundary-->>Router: Redirect to signin?redirectTo=...
  else 4xx/5xx
    Boundary->>Sentry: captureException(e) (non-4xx route/generic)
    Boundary-->>User: Render ErrorBlock (Card + actions)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

fdm-app, branch:development

Suggested reviewers

  • SvenVw
  • gerardhros

Poem

I hop through routes where headers used to sprawl,
Now one smart hare composes them all.
Errors don’t shout—they calmly return,
In a tidy card where lessons we learn.
A tractor rolls by, animation in stride—
Ship it, nibble, nap—then another ride! 🐇🚜

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Move page header to the same component as the page sidebar" is concise and accurately describes the primary change in this PR — consolidating header rendering into a sidebar-aligned component (e.g., HeaderAutomatic) and removing per-route header code — so a reviewer scanning history will understand the main intent.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FDM231-header

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@BoraIneviNMI BoraIneviNMI linked an issue Sep 22, 2025 that may be closed by this pull request
4 tasks
@coderabbitai coderabbitai Bot added branch:development Issue only affecting development, not the main branch (yet) fdm-app labels Sep 22, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Sep 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.77%. Comparing base (c44ca43) to head (7a08934).
⚠️ Report is 91 commits behind head on development.

Additional details and impacted files
@@             Coverage Diff              @@
##           development     #273   +/-   ##
============================================
  Coverage        92.77%   92.77%           
============================================
  Files               87       87           
  Lines            13030    13030           
  Branches          1289     1289           
============================================
  Hits             12089    12089           
  Misses             939      939           
  Partials             2        2           
Flag Coverage Δ
fdm-calculator 94.15% <ø> (ø)
fdm-core 91.72% <ø> (ø)
fdm-data 94.37% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (30)
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (2)

73-81: Remove unused getFarms call (wasted query + unintended hard-fail).

You fetch farms only to 404 if empty, but never use the result. This adds latency and can block the page even though the header is now self‑contained.

Apply this diff:

-import { getFarm, getFarms, getFields } from "@svenvw/fdm-core"
+import { getFarm, getFields } from "@svenvw/fdm-core"
@@
-        // Get a list of possible farms of the user
-        const farms = await getFarms(fdm, session.principal_id)
-        if (!farms || farms.length === 0) {
-            throw data("not found: farms", {
-                status: 404,
-                statusText: "not found: farms",
-            })
-        }
+        // Note: farm list not needed here; header handles its own options now.

Also applies to: 6-6


101-105: Stabilize asyncData shape to avoid undefined destructuring.

When calendar !== "2025" you return {}, but Norms destructures fields unconditionally. Return a stable object with nulls.

-            // Currently only 2025 is supported
-            if (calendar !== "2025") {
-                return {}
-            }
+            // Currently only 2025 is supported
+            if (calendar !== "2025") {
+                return {
+                    errorMessage: null,
+                    fieldNorms: null,
+                    farmNorms: null,
+                }
+            }
@@
-            return {
-                errorMessage: errorMessage,
-                fieldNorms: fieldNorms,
-                farmNorms: farmNorms,
-            }
+            return {
+                errorMessage,
+                fieldNorms,
+                farmNorms,
+            }

Also applies to: 160-166, 213-215

fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop._index.tsx (2)

229-274: Non-POST paths fall through without a response (405 recommended)

If the action receives anything other than POST, it returns undefined. Add a 405 guard (or implement DELETE as documented).

-        if (request.method === "POST") {
+        if (request.method === "POST") {
           ...
           return dataWithSuccess(
             { result: "Data saved successfully" },
             "Gewas is bijgewerkt! 🎉",
           )
-        }
+        } else {
+            return data("Method Not Allowed", {
+                status: 405,
+                statusText: "Use POST",
+            })
+        }

254-265: Updates to variety/residue are ignored unless start/end is set

Gating the update on only b_lu_start/b_lu_end skips legitimate updates to m_cropresidue and b_lu_variety.

-                    if (formValues.b_lu_start || formValues.b_lu_end) {
+                    if (
+                      formValues.b_lu_start ||
+                      formValues.b_lu_end ||
+                      typeof formValues.m_cropresidue !== "undefined" ||
+                      formValues.b_lu_variety
+                    ) {
fdm-app/app/routes/farm.$b_id_farm.settings.derogation.tsx (1)

53-56: Validate and bound-check the posted year (trust-boundary input).

Number(formData.get("year")) may yield NaN or an out-of-policy year; server-side validation prevents accidental writes and clearer errors.

Apply:

-        const formData = await request.formData()
-        const year = Number(formData.get("year"))
+        const formData = await request.formData()
+        const yearRaw = formData.get("year")
+        const year = Number(yearRaw)
+        if (!Number.isInteger(year) || year < EARLIEST_DEROGATION_YEAR || year > LATEST_DEROGATION_YEAR) {
+            return dataWithError({}, `Ongeldig jaar: ${String(yearRaw)}.`)
+        }
fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.new.upload.tsx (2)

165-173: Bug: falsy check rejects 0 for a_depth_lower; validate numerics once

!soilAnalysis.a_depth_lower wrongly rejects 0. Align checks with a_depth_upper and parse once to numbers.

Apply:

-        if (!soilAnalysis.a_depth_lower) {
-            throw new Error("Missing required a_depth_lower value")
-        }
+        const depthLower = Number(soilAnalysis.a_depth_lower)
+        if (soilAnalysis.a_depth_lower == null || Number.isNaN(depthLower)) {
+            throw new Error("Missing or invalid a_depth_lower value")
+        }
         if (!soilAnalysis.b_sampling_date) {
             throw new Error("Missing required b_sampling_date")
         }
-        if (soilAnalysis.a_depth_upper === undefined || soilAnalysis.a_depth_upper === null) {
-            throw new Error("Missing required a_depth_upper value")
-        }
+        const depthUpper = Number(soilAnalysis.a_depth_upper)
+        if (soilAnalysis.a_depth_upper == null || Number.isNaN(depthUpper)) {
+            throw new Error("Missing or invalid a_depth_upper value")
+        }
@@
-            Number(soilAnalysis.a_depth_lower),
+            depthLower,
@@
-            Number(soilAnalysis.a_depth_upper),
+            depthUpper,

Also applies to: 176-186


123-146: Add upload size limit to prevent memory/DoS issues

You read the full file into memory. Enforce a sane max (e.g., 10MB) before processing.

Apply:

         const uploadHandler = async (fileUpload: FileUpload) => {
             if (
                 fileUpload.fieldName === "soilAnalysisFile" &&
                 fileUpload.type === "application/pdf"
             ) {
+                const MAX_PDF_BYTES = 10 * 1024 * 1024 // 10MB
                 // Check file type based on magic bytes
                 const fileBuffer = await fileUpload.arrayBuffer()
+                if (fileBuffer.byteLength > MAX_PDF_BYTES) {
+                    throw new Error("File too large")
+                }
                 const fileType = await fileTypeFromBuffer(fileBuffer)
fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx (2)

283-285: Newline sanitization bug – regex matches the characters “\” and “n”, not actual newlines.

Use \n (and consider \r) and trim the result.

Apply this diff:

-        const b_address_farm = formValues.b_address_farm?.replace(/\\n/g, " ")
+        const b_address_farm = formValues.b_address_farm
+            ? formValues.b_address_farm.replace(/[\r\n]+/g, " ").trim()
+            : undefined

4-4: Replace incorrect Form import — use Form from "react-router".

File: fdm-app/app/routes/farm.$b_id_farm.settings.properties.tsx

-import { Form } from "react-hook-form"
+import { Form } from "react-router"
fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx (3)

141-147: Polygon-only assumption — MultiPolygon shapefiles will break.

You cast to FeatureCollection<Polygon,...> and map coordinates assuming Polygon. RVO/other sources can contain MultiPolygon, which will cause runtime errors or invalid geometry.

Minimal guard (return a clear warning):

         try {
             shapefile = (await combine([
                 parseShp(shpBuffer, shxBuffer),
                 parseDbf(dbfBuffer),
             ])) as FeatureCollection<Polygon, RvoProperties>
         } catch (_error) {
             return dataWithWarning({}, "Shapefile is ongeldig.")
         }
 
+        // Only Polygon geometries supported for now
+        const hasNonPolygon = shapefile.features.some(
+            (f: any) => f?.geometry?.type !== "Polygon",
+        )
+        if (hasNonPolygon) {
+            return dataWithWarning(
+                {},
+                "Shapefile bevat niet-Polygon geometrieën (bijv. MultiPolygon). Alleen Polygon wordt ondersteund.",
+            )
+        }

If preferred, I can provide a follow-up that expands MultiPolygons into individual Polygon features and imports each.

Also applies to: 158-169


190-207: Falsy checks may reject valid zero values; use nullish/empty checks.

Properties like VOLGNR could be 0, which your current !VOLGNR test treats as missing.

-            if (
-                !SECTORID ||
-                !SECTORVER ||
-                !NEN3610ID ||
-                !VOLGNR ||
-                !NAAM ||
-                !BEGINDAT ||
-                !EINDDAT ||
-                !GEWASCODE ||
-                !GEWASOMSCH ||
-                !TITEL ||
-                !TITELOMSCH
-            ) {
+            if (
+                [
+                    SECTORID,
+                    SECTORVER,
+                    NEN3610ID,
+                    VOLGNR,
+                    NAAM,
+                    BEGINDAT,
+                    EINDDAT,
+                    GEWASCODE,
+                    GEWASOMSCH,
+                    TITEL,
+                    TITELOMSCH,
+                ].some((v) => v == null || (typeof v === "string" && v.trim() === ""))
+            ) {

115-120: Remove temporary uploads after parsing — call await fileStorage.remove(storageKey).
LocalFileStorage exposes remove(key); add await fileStorage.remove(storageKey) after parsing and run it from a finally block (or equivalent) so files are deleted even on errors.
Location: fdm-app/app/routes/farm.create.$b_id_farm.$calendar.upload.tsx (around lines 115–120)

fdm-app/app/routes/farm.create.$b_id_farm.$calendar.cultivations.$b_lu_catalogue.crop.harvest.$b_id_harvesting.tsx (3)

216-236: Bug: updates are scoped by the new date, not the original date — can no-op or partially update.

When the user changes the harvest date, you filter by the newly submitted date. If no harvests yet exist on that new date, nothing gets updated (including the current record). You should anchor the group by the target harvest’s original date (same approach as your DELETE branch), then update all in that group to the new values/date.

Apply:

-            // Update harvests for all cultivations that share the same harvest date for this cultivation
-            const targetHarvests = cultivation.fields.flatMap(
-                (field: {
-                    harvests: {
-                        b_id_harvesting: string
-                        b_lu_harvest_date: Date
-                    }[]
-                }) => {
-                    return field.harvests.filter(
-                        (harvest: {
-                            b_id_harvesting: string
-                            b_lu_harvest_date: Date
-                        }) => {
-                            return (
-                                harvest.b_lu_harvest_date.getTime() ===
-                                b_lu_harvest_date.getTime()
-                            )
-                        },
-                    )
-                },
-            )
+            // Determine original anchor date from the selected harvest, then update all linked ones
+            const harvestsForCultivation =
+                cultivation.fields.flatMap(
+                    (field: {
+                        harvests: {
+                            b_id_harvesting: string
+                            b_lu_harvest_date: Date
+                        }[]
+                    }) => field.harvests,
+                ) || []
+
+            const anchorHarvest = harvestsForCultivation.find(
+                (h) => h.b_id_harvesting === b_id_harvesting,
+            )
+            if (!anchorHarvest) {
+                throw new Error("Target harvest not found")
+            }
+            const originalHarvestDate = anchorHarvest.b_lu_harvest_date
+
+            const targetHarvests = harvestsForCultivation.filter(
+                (h) =>
+                    h.b_lu_harvest_date.getTime() ===
+                    originalHarvestDate.getTime(),
+            )

241-249: Use the normalized date when calling updateHarvest.

Currently the call passes b_lu_harvest_date directly. After normalizing (see below), use harvestDate for consistency.

-                        await updateHarvest(
+                        await updateHarvest(
                             fdm,
                             session.principal_id,
                             targetHarvest.b_id_harvesting,
-                            b_lu_harvest_date,
+                            harvestDate,
                             b_lu_yield,
                             b_lu_n_harvestable,
                         )

199-201: Normalize b_lu_harvest_date to a Date to avoid .getTime() on strings.

Depending on FormSchema, this may arrive as a string. Normalize once and reuse.

-            const { b_lu_yield, b_lu_n_harvestable, b_lu_harvest_date } =
-                formValues
+            const { b_lu_yield, b_lu_n_harvestable, b_lu_harvest_date } =
+                formValues
+            const harvestDate =
+                b_lu_harvest_date instanceof Date
+                    ? b_lu_harvest_date
+                    : new Date(b_lu_harvest_date)
fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.tsx (1)

191-213: No 405 for unsupported methods.

If the method is neither POST nor DELETE, the action returns undefined. Return a 405.

Apply:

         if (request.method === "DELETE") {
           const formData = await request.formData()
           const b_lu = formData.get("b_lu")
@@
           return dataWithSuccess(
             { result: "Data deleted successfully" },
             { message: "Gewas is verwijderd" }
           )
         }
+        // Fallback for unsupported methods
+        return new Response("Method Not Allowed", {
+            status: 405,
+            headers: { Allow: "POST, DELETE" },
+        })

Also applies to: 230-233

fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.custom.tsx (2)

9-14: Prefer json over data; align with RR/Remix helpers.

Use json for 4xx responses and drop data. This also simplifies downstream error handling.

-import {
-    type ActionFunctionArgs,
-    data,
-    type LoaderFunctionArgs,
-    type MetaFunction,
-    useLoaderData,
-} from "react-router"
+import {
+    type ActionFunctionArgs,
+    json,
+    type LoaderFunctionArgs,
+    type MetaFunction,
+    useLoaderData,
+} from "react-router"

41-46: Avoid double-handling thrown Responses in loader; pass-through Response.

Throw a json Response for the param check and rethrow native Responses in catch to preserve status/body (important for localized boundaries).

-        if (!b_id_farm) {
-            throw data("invalid: b_id_farm", {
-                status: 400,
-                statusText: "invalid: b_id_farm",
-            })
-        }
+        if (!b_id_farm) {
+            throw json("invalid: b_id_farm", {
+                status: 400,
+                statusText: "invalid: b_id_farm",
+            })
+        }
-    } catch (error) {
-        throw handleLoaderError(error)
-    }
+    } catch (error) {
+        if (error instanceof Response) throw error
+        throw handleLoaderError(error)
+    }

Also applies to: 109-111

fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.cultivation.$b_lu.harvest.$b_id_harvesting.tsx (1)

196-201: Add a 405 fallback to avoid undefined returns on unsupported methods.

If a non-POST/DELETE request reaches the action, it currently falls through with no return.

Apply this diff:

       // Get the action from the form
+      // Only allow supported methods; otherwise return 405
+      if (request.method !== "POST" && request.method !== "DELETE") {
+        return data("Method Not Allowed", {
+          status: 405,
+          statusText: "Method Not Allowed",
+        })
+      }
       if (request.method === "POST") {
fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id._index.tsx (1)

492-511: POST flow can return undefined when no cultivations exist

If cultivations is empty, the function reaches the end of the try without returning a response.

             const cultivations = await getCultivations(
                 fdm,
                 session.principal_id,
                 b_id,
                 timeframe,
             )
-            if (cultivations && cultivations.length > 0) {
+            if (cultivations && cultivations.length > 0) {
                 await updateCultivation(
                     fdm,
                     session.principal_id,
                     cultivations[0].b_lu,
                     formValues.b_lu_catalogue,
                     undefined,
                     undefined,
                 )
 
                 return dataWithSuccess("fields have been updated", {
                     message: `${formValues.b_name} is bijgewerkt! 🎉`,
                 })
-            }
+            }
+            // No cultivations present — still return success for field update
+            return dataWithSuccess("field has been updated", {
+                message: `${formValues.b_name} is bijgewerkt! 🎉`,
+            })
fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx (2)

78-79: Missing 404 when field not found

Add a 404 like other routes to avoid downstream undefined usages.

-        const field = await getField(fdm, session.principal_id, b_id)
+        const field = await getField(fdm, session.principal_id, b_id)
+        if (!field) {
+            throw data("Field not found", {
+                status: 404,
+                statusText: "Field not found",
+            })
+        }

108-110: Don’t throw handleLoaderError inside asyncData

handleLoaderError returns a Response; throwing it here ends up as “[object Response]” in your errorMessage path. Throw a normal Error and let the local catch produce the message.

-                if (!cultivations.length) {
-                    throw handleLoaderError("missing: cultivations")
-                }
+                if (!cultivations.length) {
+                    throw new Error("Missing cultivations")
+                }
fdm-app/app/routes/farm.create._index.tsx (1)

82-84: Hard-coded year 2025 will break flows in 2026

Use the current year dynamically in validation, option lists, and derogation year range.

-        .max(2025, {
-            message: "Startjaar mag maximaal 2025 zijn",
-        })
+        .max(new Date().getFullYear(), {
+            message: `Startjaar mag maximaal ${new Date().getFullYear()} zijn`,
+        })
@@
-                                                                            {Array.from(
-                                                                                {
-                                                                                    length:
-                                                                                        2025 -
+                                                                            {Array.from(
+                                                                                {
+                                                                                    length:
+                                                                                        new Date().getFullYear() -
                                                                                         2006 +
                                                                                         1,
                                                                                 },
@@
-            const years = Array.from(
-                { length: 2025 - derogation_start_year + 1 },
-                (_, i) => derogation_start_year + i,
-            )
+            const currentYear = new Date().getFullYear()
+            const years = Array.from(
+                { length: currentYear - derogation_start_year + 1 },
+                (_, i) => derogation_start_year + i,
+            )

Also applies to: 227-230, 375-378

fdm-app/app/components/blocks/header/fertilizer.tsx (2)

62-79: Use the correct dropdown item type; avoid checkbox semantics for navigation.

DropdownMenuCheckboxItem implies toggle state and expects onCheckedChange; here we only navigate. Prefer DropdownMenuItem (or a radio group if you want selection semantics).

-                                {fertilizerOptions.map((option) => (
-                                    <DropdownMenuCheckboxItem
-                                        checked={p_id === option.p_id}
-                                        key={option.p_id}
-                                    >
+                                {fertilizerOptions.map((option) => (
+                                    <DropdownMenuItem key={option.p_id}>
                                         <NavLink

If you need single‑selection semantics, switch to DropdownMenuRadioGroup/DropdownMenuRadioItem.


67-74: Fragile path replacement.

currentPath.replace(p_id, option.p_id) can replace unintended substrings. Build the route explicitly.

-                                                p_id
-                                                    ? currentPath.replace(
-                                                          p_id,
-                                                          option.p_id,
-                                                      )
+                                                p_id
+                                                    ? `/farm/${b_id_farm}/fertilizers/${option.p_id}`
                                                     : `/farm/${b_id_farm}/fertilizers/${option.p_id}/`

Also consider normalizing trailing slashes.

fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.tsx (2)

141-148: Prop mismatch: fertilizerOptions is passed but never returned from loader.

This will be undefined (or a TS error). Either stop passing it or return it from the loader.

Apply this diff to return options from the loader and wire them through:

@@
     const fertilizers = await getFertilizers(
         fdm,
         session.principal_id,
         b_id_farm,
     )
+    const fertilizerOptions = fertilizers.map((f) => ({
+        p_id: f.p_id,
+        p_name_nl: f.p_name_nl ?? null,
+    }))
@@
     return {
         b_lu_catalogue: b_lu_catalogue,
         b_id_farm: b_id_farm,
         fertilizerApplications: fertilizerApplications,
         dose: dose.dose,
         applicationMethodOptions: applicationMethods.options,
+        fertilizerOptions,
     }
@@
             <FertilizerApplicationCard
                 fertilizerApplications={loaderData.fertilizerApplications}
                 applicationMethodOptions={loaderData.applicationMethodOptions}
                 fertilizerOptions={loaderData.fertilizerOptions}
                 dose={loaderData.dose}
             />

97-103: Date comparison may crash if p_app_date is a string.

Guard by coercing to Date before calling getTime().

-                    app1.p_app_date.getTime() === app2.p_app_date.getTime()
+                    new Date(app1.p_app_date).getTime() ===
+                    new Date(app2.p_app_date).getTime()
fdm-app/app/routes/farm.$b_id_farm.fertilizers.$p_id.tsx (2)

71-73: Guard against missing fertilizer to avoid runtime crash in defaultValues.

If getFertilizer returns null/undefined, the subsequent defaultValues access will throw. Add a 404 guard.

-        const fertilizer = await getFertilizer(fdm, p_id)
-        const fertilizerParameters = getFertilizerParametersDescription()
+        const fertilizer = await getFertilizer(fdm, p_id)
+        if (!fertilizer) {
+            throw data("not found: p_id", { status: 404, statusText: "not found: p_id" })
+        }
+        const fertilizerParameters = getFertilizerParametersDescription()

Optionally harden defaultValues for nullable fields to avoid controlled/uncontrolled warnings if the API returns null:

-            p_name_nl: fertilizer.p_name_nl,
+            p_name_nl: fertilizer.p_name_nl ?? "",
-            p_density: fertilizer.p_density,
+            p_density: fertilizer.p_density ?? undefined,
# apply similar `??` fallbacks only where the API can return null

Also applies to: 101-151


194-203: Guard against missing p_id_catalogue before update.

If a farm-sourced fertilizer lacks a catalogue link, the update may fail server-side. Fail fast with a clear error.

-        const p_id_catalogue = fertilizer.p_id_catalogue
+        const p_id_catalogue = fertilizer.p_id_catalogue
+        if (!p_id_catalogue) {
+            throw new Error("missing: p_id_catalogue")
+        }
fdm-app/app/routes/farm.$b_id_farm.fertilizers.new.$p_id.tsx (1)

69-72: Guard against missing fertilizer to avoid runtime crash in form defaults.

If getFertilizer returns null/undefined, defaultValues dereferences will throw. Add a 404 when not found.

-        const fertilizer = await getFertilizer(fdm, p_id)
-        const fertilizerParameters = getFertilizerParametersDescription()
+        const fertilizer = await getFertilizer(fdm, p_id)
+        if (!fertilizer) {
+            throw data("not found: p_id", {
+                status: 404,
+                statusText: "not found: p_id",
+            })
+        }
+        const fertilizerParameters = getFertilizerParametersDescription()

import { type UIMatch, useLocation, useMatches, useParams } from "react-router"
import { useCalendarStore } from "@/app/store/calendar"
import { useFarmFieldOptionsStore } from "@/app/store/farm-field-options"
import type { FertilizerOption } from "../farm/farm"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix fertilizer options typing/import and align with HeaderFertilizer.

  • The import FertilizerOption path looks suspicious and the local fertilizerOptions is typed as unknown[], but HeaderFertilizer expects a concrete option shape. This will either break TS or mask bugs.

Apply this diff to type things correctly (assuming HeaderFertilizerOption is exported from ./fertilizer; if not, export it there or declare a local type with p_id/p_name_nl):

-import type { FertilizerOption } from "../farm/farm"
+import type { HeaderFertilizerOption } from "./fertilizer"
@@
-    let fertilizerOptions: unknown[] | undefined
+    let fertilizerOptions: HeaderFertilizerOption[] | undefined
@@
-                    fertilizerOptions={
+                    fertilizerOptions={
                         /\/new(\/|$)/.test(location.pathname)
                             ? []
                             : fertilizerOptions
                     }

If HeaderFertilizerOption is not exported yet, add this to fdm-app/app/components/blocks/header/fertilizer.tsx:

+export type HeaderFertilizerOption = {
+  p_id: string
+  p_name_nl: string | undefined | null
+}

Also applies to: 32-33, 114-122

🤖 Prompt for AI Agents
In fdm-app/app/components/blocks/header/automatic.tsx around lines 5 (and
likewise address lines 32-33 and 114-122), the import and typing for fertilizer
options are incorrect: replace the loose/unknown[] typing and the suspicious
import path with the concrete HeaderFertilizerOption type (imported from
./fertilizer); if HeaderFertilizerOption is not exported from
fdm-app/app/components/blocks/header/fertilizer.tsx, export it there (or declare
a local type with properties p_id and p_name_nl) and then update the import at
the top of automatic.tsx to import { HeaderFertilizerOption } from
"./fertilizer" and change fertilizerOptions' type from unknown[] to
HeaderFertilizerOption[] and ensure all usages match that shape so
HeaderFertilizer receives the correct typed options.

Comment on lines +27 to +35
if (error.status === 401) {
// Get the current path the user tried to access
const currentPath =
location.pathname + location.search + location.hash
// Construct the sign-in URL with the redirectTo parameter
const signInUrl = `./signin?redirectTo=${encodeURIComponent(currentPath)}`
// Throw the redirect response to be caught by React Router
throw redirect(signInUrl)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

❓ Verification inconclusive

401 redirect path should be absolute; relative “./signin” may 404 under nested routes.

Use /signin and consider navigation instead of throwing a redirect from a component.

-            const signInUrl = `./signin?redirectTo=${encodeURIComponent(currentPath)}`
+            const signInUrl = `/signin?redirectTo=${encodeURIComponent(currentPath)}`
-            // Throw the redirect response to be caught by React Router
-            throw redirect(signInUrl)
+            // Prefer client navigation from components; throwing redirect is for loaders/actions.
+            // If you want to keep throw-behavior, verify it works with RRv7 in component boundaries.
+            throw redirect(signInUrl)

Please verify the behavior with RR v7. If throws from component boundaries don’t navigate, switch to useNavigate:

// inside InlineErrorBoundary
const navigate = useNavigate()
useEffect(() => {
  if (isRouteErrorResponse(error) && error.status === 401) {
    const currentPath = location.pathname + location.search + location.hash
    navigate(`/signin?redirectTo=${encodeURIComponent(currentPath)}`, { replace: true })
  }
}, [error, location, navigate])
return null

Use absolute /signin URL and client‐side navigation in InlineErrorBoundary
Replace the relative ./signin with /signin to avoid nested‐route resolution errors(docs4dev.com), and handle 401 redirects in the component using useNavigate (rather than throwing redirect(), which is intended for loaders/actions)(reactrouter.com).

🤖 Prompt for AI Agents
In fdm-app/app/components/custom/inline-error-boundary.tsx around lines 27 to
35, the code currently throws a redirect to a relative "./signin" which can
resolve incorrectly in nested routes and uses redirect() (meant for
loaders/actions) instead of client navigation; change the URL to an absolute
"/signin" and replace the thrown redirect with client-side navigation via the
useNavigate hook (call
navigate(`/signin?redirectTo=${encodeURIComponent(currentPath)}`) when
error.status === 401), ensuring you call the hook at component scope and perform
the navigate side effect (and then return/stop rendering the error UI as
appropriate).

Comment on lines 90 to 92
} catch (error) {
throw handleActionError(error)
return handleActionError(error)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Throw from loader catch; returning breaks error boundaries and crashes the UI

Returning handleActionError(error) causes the route component to render with an error-shaped payload where loaderData.field is expected, leading to runtime failures in getViewState(fields) and MapGL props. To preserve ErrorBoundary flow (and match existing loader semantics), throw the response instead.

-        return handleActionError(error)
+        throw handleActionError(error)

If the new pattern truly requires returning structured errors, then the component must guard before using loaderData.field and render an inline error state. Verify ~/lib/error.handleActionError contract and repo-wide usage:


🏁 Script executed:

#!/bin/bash
# 1) Inspect implementation
rg -nC2 'export (async )?function handleActionError|export const handleActionError' --type=ts

# 2) Where is it thrown vs returned?
rg -nC1 'handleActionError\(' --type=ts | sed -n 's/.*:\([0-9]\+\):.*/\1/p' >/dev/null
rg -nP 'throw\s+handleActionError\(' --type=ts
rg -nP 'return\s+handleActionError\(' --type=ts

# 3) Check for error guards in this route component
rg -n 'InlineErrorBoundary|useRouteError|isRouteErrorResponse|useActionData' fdm-app/app/routes/farm.\$b_id_farm.\$calendar.field.\$b_id.atlas.tsx -n

Length of output: 5271


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) All occurrences of handleActionError:"
rg -n --hidden -S "handleActionError" || true
echo

echo "2) Look for explicit definitions (function/const/let/export default):"
rg -n --hidden -S "^[[:space:]]*(export )?(const|let|function)[[:space:]]+handleActionError" || true
rg -n --hidden -S "export default .*handleActionError" || true
echo

echo "3) If no explicit def found, list unique files that reference it:"
rg -n --hidden -S "handleActionError" | awk -F: '{print $1}' | sort -u > /tmp/ha_files || true
printf "%s\n" "$(cat /tmp/ha_files)" || true
echo

echo "4) Print the first 200 lines of each file that references handleActionError (to find its implementation/contract):"
while read -r f; do
  echo "---- $f ----"
  sed -n '1,200p' "$f" || true
done < /tmp/ha_files
echo

echo "5) Show the atlas route file around the relevant catches:"
atlas='fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.atlas.tsx'
if [ -f "$atlas" ]; then
  rg -n "handleActionError" "$atlas" || true
  sed -n '1,260p' "$atlas"
else
  echo "File not found: $atlas"
fi

Length of output: 264280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the error handling file
error_file=$(rg -l "handleActionError" -g "fdm-app/lib/error.ts")
echo "Error file: $error_file"

# Print the implementation of handleActionError
sed -n '1,200p' "$error_file"

Length of output: 148


Throw from loader catch; returning breaks error boundaries and crashes the UI

Returning handleActionError(error) in a loader places an error-shaped payload into loaderData (bypassing the route ErrorBoundary) — throw the response so the ErrorBoundary/loader semantics are preserved.

-        return handleActionError(error)
+        throw handleActionError(error)

Affected: fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.atlas.tsx (loader catch). Repo scan shows multiple occurrences of "return handleActionError(...)" — audit loaders and replace returns with throws where the loader should trigger the ErrorBoundary. Verify the ~/lib/error.handleActionError contract and repo-wide usage before mass changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
throw handleActionError(error)
return handleActionError(error)
}
} catch (error) {
throw handleActionError(error)
}
🤖 Prompt for AI Agents
In fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.atlas.tsx around
lines 90-92, the loader catch currently returns handleActionError(error) which
injects an error-shaped value into loaderData and bypasses the route
ErrorBoundary; change the catch to throw the response from
handleActionError(error) (i.e., throw await or throw the returned
Response/object per handleActionError contract) so the ErrorBoundary/loader
semantics are preserved. After fixing this instance, audit other loader
functions across the repo for "return handleActionError(" and replace returns
with throws where the loader is expected to trigger ErrorBoundary, and confirm
the exact return/throw shape by checking ~/lib/error.handleActionError to ensure
callers throw the correct Response/error type.

})
} catch (error) {
throw handleActionError(error)
return handleActionError(error)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Returning handleActionError from the action is the right move—verify consumer and payload shape.

Good shift from throwing to returning; this enables inline form errors instead of tripping the route boundary. Please verify that:

  • handleActionError returns serializable action data (not a thrown Response) with appropriate status codes (400/422/500).
  • SoilAnalysisForm on this route reads useActionData and surfaces these errors.

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate handleActionError definition:"
rg -nP -C2 '(^export\s+(?:async\s+)?function\s+handleActionError\b)|(\bconst\s+handleActionError\s*=\s*\()' --type ts --type tsx

echo "Call sites of handleActionError:"
rg -nP -C2 '\bhandleActionError\s*\(' --type ts --type tsx -g '!**/node_modules/**'

echo "Confirm SoilAnalysisForm consumes action data:"
rg -nP -C3 'export (?:const|function) SoilAnalysisForm' --type tsx
rg -nP -C3 '\buseActionData\b' --type tsx

Length of output: 64


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for handleActionError definition (ts/tsx files):"
rg -nP -C2 '(^export\s+(?:async\s+)?function\s+handleActionError\b)|(\bconst\s+handleActionError\s*=\s*\()' -g '!**/node_modules/**' -g '*.ts' -g '*.tsx' || true

echo "Searching for all call sites of handleActionError (ts/tsx):"
rg -nP -C2 '\bhandleActionError\s*\(' -g '!**/node_modules/**' -g '*.ts' -g '*.tsx' || true

echo "Searching for SoilAnalysisForm component definitions (tsx):"
rg -nP -C3 'export (?:const|function) SoilAnalysisForm' -g '!**/node_modules/**' -g '*.tsx' || true

echo "Searching for useActionData usages (tsx):"
rg -nP -C3 '\buseActionData\b' -g '!**/node_modules/**' -g '*.tsx' || true

file='fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx'
if [ -f "$file" ]; then
  echo "Printing lines 195-215 from $file:"
  sed -n '195,215p' "$file"
else
  echo "File not found: $file"
fi

Length of output: 27519


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate all occurrences of handleActionError, dataWithError, redirectWithSuccess:"
rg -nP '\b(handleActionError|dataWithError|redirectWithSuccess)\b' -g '!**/node_modules/**' || true

echo; echo "Search for handleActionError definition (function/const/export):"
rg -nP '(^export\s+(?:async\s+)?function\s+handleActionError\b)|(\b(?:const|let|var)\s+handleActionError\s*=\s*\()|(\bexport\s+const\s+handleActionError\b)' -g '!**/node_modules/**' || true

echo; echo "Search for dataWithError / redirectWithSuccess definitions:"
rg -nP '(^export\s+(?:async\s+)?function\s+(dataWithError|redirectWithSuccess)\b)|(\b(?:const|let|var)\s+(dataWithError|redirectWithSuccess)\s*=\s*\()|(\bexport\s+const\s+(dataWithError|redirectWithSuccess)\b)' -g '!**/node_modules/**' || true

echo; echo "List helper files that reference these helpers:"
rg -n --hidden -S 'handleActionError|dataWithError|redirectWithSuccess' -g '!**/node_modules/**' -l || true

echo; echo "Print likely helper files (first 240 lines) to inspect return shapes:"
for f in $(rg -nP 'handleActionError|dataWithError|redirectWithSuccess' -g '!**/node_modules/**' -l || true); do
  echo "---- $f ----"
  sed -n '1,240p' "$f" || true
  echo
done

echo "Print SoilAnalysisForm (component) for useActionData consumption:"
sed -n '1,240p' fdm-app/app/components/blocks/soil/form.tsx || echo "file not found"
echo
echo "Also print soil form-upload (already known to use useActionData) for reference:"
sed -n '1,240p' fdm-app/app/components/blocks/soil/form-upload.tsx || echo "file not found"

Length of output: 27906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files containing 'handleActionError':"
rg -n --hidden -S "handleActionError" -g '!**/node_modules/**' -l || true
echo

echo "Print top of files that mention 'handleActionError' to find its definition (first 400 lines):"
for f in $(rg -n --hidden -S "handleActionError" -g '!**/node_modules/**' -l || true); do
  echo "---- $f ----"
  sed -n '1,400p' "$f" || true
  echo
done

echo "Files containing 'dataWithError' or 'redirectWithSuccess':"
rg -n --hidden -S "dataWithError|redirectWithSuccess" -g '!**/node_modules/**' -l || true
echo

echo "Print top of files that mention 'dataWithError' or 'redirectWithSuccess' (first 400 lines):"
for f in $(rg -n --hidden -S "dataWithError|redirectWithSuccess" -g '!**/node_modules/**' -l || true); do
  echo "---- $f ----"
  sed -n '1,400p' "$f" || true
  echo
done

route='fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx'
echo "Print full route file: $route"
if [ -f "$route" ]; then
  sed -n '1,400p' "$route"
else
  echo "File not found: $route"
fi

Length of output: 7572


Action returns handleActionError — confirm payload and make the form consume it

  • SoilAnalysisForm (fdm-app/app/components/blocks/soil/form.tsx) does not call useActionData, so returning handleActionError(error) from the action will not surface inline form errors.
  • Ensure handleActionError returns a serializable action payload (example used elsewhere: { message?: string; fieldErrors?: Record<string,string[]>; formErrors?: string[] }) and uses appropriate status codes (400/422 for validation, 500 for server).
  • Remediate by either updating SoilAnalysisForm to consume useActionData and map fieldErrors → form.setError / FormMessage, or by changing the action to throw/return a Response that matches the intended UX.

Files: fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx (action), fdm-app/app/components/blocks/soil/form.tsx (consumer), fdm-app/app/components/blocks/soil/form-upload.tsx (example).

🤖 Prompt for AI Agents
In
fdm-app/app/routes/farm.$b_id_farm.$calendar.field.$b_id.soil.analysis.$a_id.tsx
around line 205, the action currently returns handleActionError(error) but the
SoilAnalysisForm does not call useActionData so inline form errors never
surface; update the action to return a serializable payload shaped like {
message?: string; fieldErrors?: Record<string, string[]>; formErrors?: string[]
} and use appropriate status codes (400/422 for validation, 500 for server) OR
throw/return a Response with that JSON and status; then update
fdm-app/app/components/blocks/soil/form.tsx to call useActionData(), map
fieldErrors to form.setError or FormMessage (similar to form-upload.tsx), and
ensure the action and consumer agree on the payload shape so validation errors
display inline.

Comment on lines +28 to +33
const fertilizerOptions = fertilizers.map((fertilizer) => {
return {
p_id: fertilizer.p_id,
p_name_nl: fertilizer.p_name_nl || "",
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent empty labels: don’t coerce missing names to empty string.

Mapping p_name_nl || "" suppresses your header’s fallback label. Use nullish coalescing and keep null/undefined.

-                p_name_nl: fertilizer.p_name_nl || "",
+                p_name_nl: fertilizer.p_name_nl ?? null,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fertilizerOptions = fertilizers.map((fertilizer) => {
return {
p_id: fertilizer.p_id,
p_name_nl: fertilizer.p_name_nl || "",
}
})
const fertilizerOptions = fertilizers.map((fertilizer) => {
return {
p_id: fertilizer.p_id,
p_name_nl: fertilizer.p_name_nl ?? null,
}
})
🤖 Prompt for AI Agents
In fdm-app/app/routes/farm.$b_id_farm.fertilizers.tsx around lines 28-33 the
mapping coerces missing fertilizer names to an empty string using `p_name_nl ||
""`, which hides the header’s fallback; remove the `|| ""` and preserve
null/undefined (use `p_name_nl` or `p_name_nl ?? undefined`) so the consumer can
trigger its fallback label, and update the mapped type/interface if necessary to
allow `string | undefined`/`null`.

Comment on lines +31 to 38
export async function loader({ request }: LoaderFunctionArgs) {
try {
// Get the farm id
const b_id_farm = params.b_id_farm
if (!b_id_farm) {
throw data("Farm ID is required", {
status: 400,
statusText: "Farm ID is required",
})
}

// Get the session
const session = await getSession(request)

// Return the farm ID and session info
return {
farmId: b_id_farm,
session,
}
await getSession(request)
} catch (error) {
throw handleActionError(error)
return handleActionError(error)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Throw loader errors instead of returning, and use the loader error helper.

Returning the error response prevents the ErrorBoundary flow (401 redirect logic won’t trigger). Use handleLoaderError and throw.

-import type { LoaderFunctionArgs, MetaFunction } from "react-router"
+import type { LoaderFunctionArgs, MetaFunction } from "react-router"
@@
-import { handleActionError } from "~/lib/error"
+import { handleLoaderError } from "~/lib/error"
@@
 export async function loader({ request }: LoaderFunctionArgs) {
   try {
     // Get the session
     await getSession(request)
   } catch (error) {
-    return handleActionError(error)
+    throw handleLoaderError(error)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function loader({ request }: LoaderFunctionArgs) {
try {
// Get the farm id
const b_id_farm = params.b_id_farm
if (!b_id_farm) {
throw data("Farm ID is required", {
status: 400,
statusText: "Farm ID is required",
})
}
// Get the session
const session = await getSession(request)
// Return the farm ID and session info
return {
farmId: b_id_farm,
session,
}
await getSession(request)
} catch (error) {
throw handleActionError(error)
return handleActionError(error)
}
}
export async function loader({ request }: LoaderFunctionArgs) {
try {
// Get the session
await getSession(request)
} catch (error) {
throw handleLoaderError(error)
}
}
🤖 Prompt for AI Agents
In fdm-app/app/routes/farm.$b_id_farm.tsx around lines 31 to 38, the loader
currently catches errors and returns the result of handleActionError which
prevents the ErrorBoundary/loader flow (e.g., 401 redirect) from running;
replace the return with throwing the loader error helper by calling throw
handleLoaderError(error) inside the catch so the Remix loader error handling
runs; ensure the loader still returns the expected success value (e.g., the
session) on the happy path and import/use handleLoaderError instead of
handleActionError.

import { Separator } from "~/components/ui/separator"
import { getSession } from "~/lib/auth.server"
import { handleLoaderError } from "~/lib/error"
import { handleActionError } from "~/lib/error"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Loader uses action error helper and inspects non-existent Response.init

handleActionError is for actions; Response doesn’t expose init. This path likely never redirects and may mask real errors.

-import { handleActionError } from "~/lib/error"
+import { handleLoaderError } from "~/lib/error"
@@
-    } catch (error) {
-        const response = await handleActionError(error)
-        if (response.init) {
-            return redirect(
-                `/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}`,
-                response.init,
-            )
-        }
-        return response
-    }
+    } catch (error) {
+        throw handleLoaderError(error)
+    }

Also applies to: 58-66

Comment on lines +98 to 106
const response = await handleActionError(error)
if (response.init) {
return redirect(
`/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}/soil/analysis`,
response.init,
)
}
return response
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use handleLoaderError in loader catch; current redirect-on-error path is brittle.

handleActionError is intended for actions; checking response.init is non-standard and likely incorrect. Prefer throwing loader-shaped errors so ErrorBoundaries handle them consistently.

Apply:

-import {
-    type ActionFunctionArgs,
-    data,
-    type LoaderFunctionArgs,
-    NavLink,
-    redirect,
-    useLoaderData,
-} from "react-router"
+import {
+    type ActionFunctionArgs,
+    data,
+    type LoaderFunctionArgs,
+    NavLink,
+    useLoaderData,
+} from "react-router"
@@
-import { handleActionError } from "~/lib/error"
+import { handleActionError, handleLoaderError } from "~/lib/error"
@@
-    } catch (error) {
-        const response = await handleActionError(error)
-        if (response.init) {
-            return redirect(
-                `/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}/soil/analysis`,
-                response.init,
-            )
-        }
-        return response
-    }
+    } catch (error) {
+        throw handleLoaderError(error)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await handleActionError(error)
if (response.init) {
return redirect(
`/farm/create/${params.b_id_farm}/${params.calendar}/fields/${params.b_id}/soil/analysis`,
response.init,
)
}
return response
}
import {
type ActionFunctionArgs,
data,
type LoaderFunctionArgs,
NavLink,
useLoaderData,
} from "react-router"
import { handleActionError, handleLoaderError } from "~/lib/error"
...
} catch (error) {
throw handleLoaderError(error)
}
🤖 Prompt for AI Agents
In
fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fields.$b_id.soil.analysis.$analysis_type.tsx
around lines 98 to 106, the catch currently calls handleActionError and inspects
response.init then redirects which is brittle; replace this with using
handleLoaderError (or throwing a loader-shaped error) so the loader error
boundary handles it consistently: call handleLoaderError(error) (or transform
the caught error into the loader error shape it expects) and then throw or
return its result instead of inspecting response.init and performing a manual
redirect.

@SvenVw SvenVw closed this Feb 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch:development Issue only affecting development, not the main branch (yet) fdm-app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Error Handling with Granular Error Boundaries

2 participants