From 776c6dd74a8331fdc64f2fd704c6c841d0009f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 28 Jan 2026 14:41:22 +0100 Subject: [PATCH 1/5] Use separate rotation filter and selection state --- .../app/components/blocks/rotation/table.tsx | 90 ++++++++++--------- fdm-app/app/store/field-filter.ts | 57 ++++++------ fdm-app/app/store/rotation-selection.ts | 31 +++++++ 3 files changed, 110 insertions(+), 68 deletions(-) create mode 100644 fdm-app/app/store/rotation-selection.ts diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 8daff223b..5bd39846a 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -9,6 +9,7 @@ import { getFilteredRowModel, getSortedRowModel, type Row, + type RowSelectionState, type SortingState, useReactTable, type VisibilityState, @@ -22,8 +23,8 @@ import { NavLink, useLocation, useParams } from "react-router-dom" import { toast as notify } from "sonner" import { modifySearchParams } from "@/app/lib/url-utils" import { useActiveTableFormStore } from "@/app/store/active-table-form" -import { useFieldFilterStore } from "@/app/store/field-filter" -import { useFieldSelectionStore } from "@/app/store/field-selection" +import { useRotationFilterStore } from "@/app/store/field-filter" +import { useRotationSelectionStore } from "@/app/store/rotation-selection" import { Button } from "~/components/ui/button" import { DropdownMenu, @@ -64,7 +65,7 @@ export function DataTable({ }: DataTableProps) { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) - const fieldFilter = useFieldFilterStore() + const fieldFilter = useRotationFilterStore() const isMobile = useIsMobile() const [columnVisibility, setColumnVisibility] = useState( isMobile @@ -74,9 +75,11 @@ export function DataTable({ const lastSelectedRowIndex = useRef(null) const location = useLocation() - const fieldIds = useFieldSelectionStore((state) => state.fieldIds) - const setFieldIds = useFieldSelectionStore((state) => state.setFieldIds) - const syncFarm = useFieldSelectionStore((state) => state.syncFarm) + const selection = useRotationSelectionStore((state) => state.selection) + const setSelection = useRotationSelectionStore( + (state) => state.setSelection, + ) + const syncFarm = useRotationSelectionStore((state) => state.syncFarm) useEffect(() => { setColumnVisibility( @@ -101,6 +104,24 @@ export function DataTable({ (store) => store.clearActiveForm, ) + function handleSelection(rowSelection: RowSelectionState) { + // Sync to store + const newSelection = Object.fromEntries( + table + .getFilteredRowModel() + .rows.map((row) => [ + (row.original as CropRow).b_lu_catalogue, + Object.fromEntries( + row.subRows.map((fieldRow) => [ + (fieldRow.original as FieldRow).b_id, + rowSelection[fieldRow.id], + ]), + ), + ]), + ) + setSelection(newSelection) + } + const handleRowClick = ( row: Row, event: React.MouseEvent, @@ -151,11 +172,7 @@ export function DataTable({ } } - // Sync to store - const newFieldIds = Object.keys(newRowSelection).filter( - (k) => !k.startsWith("crop_") && newRowSelection[k], - ) - setFieldIds(newFieldIds) + handleSelection(newRowSelection) } } else { lastSelectedRowIndex.current = null @@ -222,33 +239,23 @@ export function DataTable({ } const rowSelection = useMemo(() => { - const sel: Record = {} - for (const id of fieldIds) { - sel[id] = true - } - for (const row of memoizedData) { - if (row.type === "crop") { - let all = true - let has = false - for (const field of row.fields) { - has = true - if (fieldIds.includes(field.b_id)) { - sel[field.b_id] = true - } else { - all = false - } - } - if (has && all) { - sel[`crop_${row.b_lu_catalogue}`] = true - } - } else { - if (fieldIds.includes(row.b_id)) { - sel[row.b_id] = true - } - } - } - return sel - }, [fieldIds, memoizedData]) + return Object.fromEntries([ + // Crop selection state is derived from whether all its fields are selected + ...memoizedData.map((crop) => [ + `crop_${crop.b_lu_catalogue}`, + crop.fields.every( + (field) => selection[crop.b_lu_catalogue]?.[field.b_id], + ), + ]), + // Include each field's selection state too + ...memoizedData.flatMap((crop) => + crop.fields.map((field) => [ + field.b_id, + selection[crop.b_lu_catalogue]?.[field.b_id], + ]), + ), + ]) + }, [selection, memoizedData]) const table = useReactTable({ data: memoizedData, @@ -274,10 +281,7 @@ export function DataTable({ }, onRowSelectionChange: (fn) => { const selection = typeof fn === "function" ? fn(rowSelection) : fn - const newFieldIds = Object.keys(selection).filter( - (k) => !k.startsWith("crop_") && selection[k], - ) - setFieldIds(newFieldIds) + handleSelection(selection) }, globalFilterFn: fuzzySearchAndProductivityFilter, // There are nulls in the columns which can cause false assumptions if this is not provided @@ -582,4 +586,4 @@ export function DataTable({ ) -} \ No newline at end of file +} diff --git a/fdm-app/app/store/field-filter.ts b/fdm-app/app/store/field-filter.ts index 0dfb1ba9e..0b2e3af0d 100644 --- a/fdm-app/app/store/field-filter.ts +++ b/fdm-app/app/store/field-filter.ts @@ -11,30 +11,37 @@ interface FieldFilterState { syncFarm: (farmId: string) => void } -export const useFieldFilterStore = create()( - persist( - (set, get) => ({ - farmId: null, - showProductiveOnly: false, // Default to showing all fields - searchTerms: "", - toggleShowProductiveOnly: () => - set((state) => ({ - showProductiveOnly: !state.showProductiveOnly, - })), - setSearchTerms: (value) => { - set({ - searchTerms: value, - }) +const createFilterStore = (name: string) => + create()( + persist( + (set, get) => ({ + farmId: null, + showProductiveOnly: false, // Default to showing all fields + searchTerms: "", + toggleShowProductiveOnly: () => + set((state) => ({ + showProductiveOnly: !state.showProductiveOnly, + })), + setSearchTerms: (value) => { + set({ + searchTerms: value, + }) + }, + syncFarm(farmId: string) { + if (get().farmId !== farmId) { + set({ farmId, searchTerms: "" }) + } + }, + }), + { + name: name, // unique name + storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage }, - syncFarm(farmId: string) { - if (get().farmId !== farmId) { - set({ farmId, searchTerms: "" }) - } - }, - }), - { - name: "field-filter-storage", // unique name - storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage - }, - ), + ), + ) + +export const useFieldFilterStore = createFilterStore("field-filter-storage") + +export const useRotationFilterStore = createFilterStore( + "rotation-filter-storage", ) diff --git a/fdm-app/app/store/rotation-selection.ts b/fdm-app/app/store/rotation-selection.ts new file mode 100644 index 000000000..d9b19030e --- /dev/null +++ b/fdm-app/app/store/rotation-selection.ts @@ -0,0 +1,31 @@ +import { create } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" +import { ssrSafeSessionJSONStorage } from "./storage" + +interface FieldSelectionState { + farmId: string | null + selection: Record> + setSelection: (selection: Record>) => void + syncFarm: (farmId: string) => void +} + +export const useRotationSelectionStore = create()( + persist( + (set, get) => ({ + farmId: null, + selection: {}, + setSelection(selection: Record>) { + set({ selection }) + }, + syncFarm(farmId: string) { + if (get().farmId !== farmId) { + set({ farmId, selection: {} }) + } + }, + }), + { + name: "rotation-selection-storage", // unique name + storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage + }, + ), +) From 2817551d4db3f6cf22e610d62cfcf33984022fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 28 Jan 2026 14:54:02 +0100 Subject: [PATCH 2/5] Consider filters when building the rowSelection --- fdm-app/app/components/blocks/rotation/table.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 5bd39846a..b0f25d420 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -238,13 +238,19 @@ export function DataTable({ ) } + // biome-ignore lint/correctness/useExhaustiveDependencies: the filter function is pure const rowSelection = useMemo(() => { return Object.fromEntries([ // Crop selection state is derived from whether all its fields are selected ...memoizedData.map((crop) => [ `crop_${crop.b_lu_catalogue}`, crop.fields.every( - (field) => selection[crop.b_lu_catalogue]?.[field.b_id], + (field) => + !fuzzySearchAndProductivityFilter( + { original: field } as unknown as Row, + null, + fieldFilter, + ) || selection[crop.b_lu_catalogue]?.[field.b_id], ), ]), // Include each field's selection state too @@ -255,7 +261,7 @@ export function DataTable({ ]), ), ]) - }, [selection, memoizedData]) + }, [selection, memoizedData, fieldFilter]) const table = useReactTable({ data: memoizedData, From bdf651211292ac71157df53ab2b4c8bfcf89982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 28 Jan 2026 15:45:25 +0100 Subject: [PATCH 3/5] Rename rotation selection state type --- fdm-app/app/store/rotation-selection.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/store/rotation-selection.ts b/fdm-app/app/store/rotation-selection.ts index d9b19030e..41eb522d5 100644 --- a/fdm-app/app/store/rotation-selection.ts +++ b/fdm-app/app/store/rotation-selection.ts @@ -2,14 +2,14 @@ import { create } from "zustand" import { createJSONStorage, persist } from "zustand/middleware" import { ssrSafeSessionJSONStorage } from "./storage" -interface FieldSelectionState { +interface RotationSelectionState { farmId: string | null selection: Record> setSelection: (selection: Record>) => void syncFarm: (farmId: string) => void } -export const useRotationSelectionStore = create()( +export const useRotationSelectionStore = create()( persist( (set, get) => ({ farmId: null, From 2a3e14da294b71e6ef538b7d7ff327c4bedfc5b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 28 Jan 2026 16:12:23 +0100 Subject: [PATCH 4/5] Merge selection state with the previous --- .../app/components/blocks/rotation/table.tsx | 6 +++--- fdm-app/app/store/rotation-selection.ts | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index b0f25d420..92b3ce73a 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -76,8 +76,8 @@ export function DataTable({ const location = useLocation() const selection = useRotationSelectionStore((state) => state.selection) - const setSelection = useRotationSelectionStore( - (state) => state.setSelection, + const updateSelection = useRotationSelectionStore( + (state) => state.updateSelection, ) const syncFarm = useRotationSelectionStore((state) => state.syncFarm) @@ -119,7 +119,7 @@ export function DataTable({ ), ]), ) - setSelection(newSelection) + updateSelection(newSelection) } const handleRowClick = ( diff --git a/fdm-app/app/store/rotation-selection.ts b/fdm-app/app/store/rotation-selection.ts index 41eb522d5..a1f97599d 100644 --- a/fdm-app/app/store/rotation-selection.ts +++ b/fdm-app/app/store/rotation-selection.ts @@ -5,7 +5,9 @@ import { ssrSafeSessionJSONStorage } from "./storage" interface RotationSelectionState { farmId: string | null selection: Record> - setSelection: (selection: Record>) => void + updateSelection: ( + selection: Record>, + ) => void syncFarm: (farmId: string) => void } @@ -14,8 +16,18 @@ export const useRotationSelectionStore = create()( (set, get) => ({ farmId: null, selection: {}, - setSelection(selection: Record>) { - set({ selection }) + updateSelection( + selection: Record>, + ) { + const currentSelection = get().selection + const result: Record> = {} + for (const currentKey of Object.keys(currentSelection)) { + result[currentKey] = { ...currentSelection[currentKey] } + } + for (const key of Object.keys(selection)) { + result[key] = { ...(result[key] ?? {}), ...selection[key] } + } + set({ selection: result }) }, syncFarm(farmId: string) { if (get().farmId !== farmId) { From 99b2d9b2e214daa2348e47ef3d5a55a581fe32e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 28 Jan 2026 16:35:06 +0100 Subject: [PATCH 5/5] Fix unique ids --- fdm-app/app/components/blocks/rotation/table.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/blocks/rotation/table.tsx b/fdm-app/app/components/blocks/rotation/table.tsx index 92b3ce73a..c3f709552 100644 --- a/fdm-app/app/components/blocks/rotation/table.tsx +++ b/fdm-app/app/components/blocks/rotation/table.tsx @@ -203,6 +203,7 @@ export function DataTable({ return { ...field, + b_lu_catalogue: (item as CropRow).b_lu_catalogue, searchTarget: `${field.b_name} ${commonTerms} ${dateTermsArr(field.b_lu_start)} ${dateTermsArr(field.b_lu_end)} ${dateTermsArr(field.b_lu_harvest_date)}`, } }) @@ -256,7 +257,7 @@ export function DataTable({ // Include each field's selection state too ...memoizedData.flatMap((crop) => crop.fields.map((field) => [ - field.b_id, + `${crop.b_lu_catalogue}_${field.b_id}`, selection[crop.b_lu_catalogue]?.[field.b_id], ]), ), @@ -267,7 +268,9 @@ export function DataTable({ data: memoizedData, columns, getRowId: (row) => - row.type === "crop" ? `crop_${row.b_lu_catalogue}` : row.b_id, + row.type === "crop" + ? `crop_${row.b_lu_catalogue}` + : `${row.b_lu_catalogue}_${row.b_id}`, getCoreRowModel: getCoreRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(),