Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 57 additions & 44 deletions fdm-app/app/components/blocks/rotation/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getFilteredRowModel,
getSortedRowModel,
type Row,
type RowSelectionState,
type SortingState,
useReactTable,
type VisibilityState,
Expand All @@ -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,
Expand Down Expand Up @@ -64,7 +65,7 @@ export function DataTable<TData extends RotationExtended, TValue>({
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const fieldFilter = useFieldFilterStore()
const fieldFilter = useRotationFilterStore()
const isMobile = useIsMobile()
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
isMobile
Expand All @@ -74,9 +75,11 @@ export function DataTable<TData extends RotationExtended, TValue>({
const lastSelectedRowIndex = useRef<string | null>(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 updateSelection = useRotationSelectionStore(
(state) => state.updateSelection,
)
const syncFarm = useRotationSelectionStore((state) => state.syncFarm)

useEffect(() => {
setColumnVisibility(
Expand All @@ -101,6 +104,24 @@ export function DataTable<TData extends RotationExtended, TValue>({
(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],
]),
),
]),
)
updateSelection(newSelection)
}
Comment on lines +107 to +123
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n fdm-app/app/components/blocks/rotation/table.tsx | head -150

Repository: SvenVw/fdm

Length of output: 6107


🏁 Script executed:

sed -n '150,250p' fdm-app/app/components/blocks/rotation/table.tsx

Repository: SvenVw/fdm

Length of output: 4004


🏁 Script executed:

sed -n '219,300p' fdm-app/app/components/blocks/rotation/table.tsx

Repository: SvenVw/fdm

Length of output: 3400


🏁 Script executed:

rg -n "setSelection|selection\[" fdm-app/app/components/blocks/rotation/table.tsx -A 2 -B 2

Repository: SvenVw/fdm

Length of output: 823


🏁 Script executed:

sed -n '107,130p' fdm-app/app/components/blocks/rotation/table.tsx | cat -n

Repository: SvenVw/fdm

Length of output: 1111


Confirm: selection state of filtered-out rows is indeed lost.

The handleSelection function builds newSelection only from table.getFilteredRowModel().rows, then calls setSelection(newSelection), which completely replaces the stored selection. Any rows currently filtered out are excluded from this new selection object, causing their selection state to be discarded.

To preserve selection for hidden rows, merge the new selection with the existing one instead of replacing it entirely:

setSelection({ ...selection, ...newSelection })
🤖 Prompt for AI Agents
In `@fdm-app/app/components/blocks/rotation/table.tsx` around lines 107 - 123, The
handleSelection function currently builds newSelection only from
table.getFilteredRowModel().rows and calls setSelection(newSelection), which
drops selection state for filtered-out rows; fix this by merging the existing
selection state with the computed newSelection instead of replacing it (use the
existing selection object available in scope or read current state) so hidden
rows are preserved when calling setSelection; ensure you create a new object
(immutable merge) rather than mutating state directly and keep references to
RowSelectionState and the fields derived from (row.original as
CropRow).b_lu_catalogue and (fieldRow.original as FieldRow).b_id to locate where
to merge.


const handleRowClick = (
row: Row<TData>,
event: React.MouseEvent<HTMLTableRowElement>,
Expand Down Expand Up @@ -151,11 +172,7 @@ export function DataTable<TData extends RotationExtended, TValue>({
}
}

// Sync to store
const newFieldIds = Object.keys(newRowSelection).filter(
(k) => !k.startsWith("crop_") && newRowSelection[k],
)
setFieldIds(newFieldIds)
handleSelection(newRowSelection)
}
} else {
lastSelectedRowIndex.current = null
Expand Down Expand Up @@ -186,6 +203,7 @@ export function DataTable<TData extends RotationExtended, TValue>({

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)}`,
}
})
Expand Down Expand Up @@ -221,40 +239,38 @@ export function DataTable<TData extends RotationExtended, TValue>({
)
}

// biome-ignore lint/correctness/useExhaustiveDependencies: the filter function is pure
const rowSelection = useMemo(() => {
const sel: Record<string, boolean> = {}
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) =>
!fuzzySearchAndProductivityFilter(
{ original: field } as unknown as Row<TData>,
null,
fieldFilter,
) || selection[crop.b_lu_catalogue]?.[field.b_id],
),
]),
// Include each field's selection state too
...memoizedData.flatMap((crop) =>
crop.fields.map((field) => [
`${crop.b_lu_catalogue}_${field.b_id}`,
selection[crop.b_lu_catalogue]?.[field.b_id],
]),
),
])
}, [selection, memoizedData, fieldFilter])

const table = useReactTable({
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(),
Expand All @@ -274,10 +290,7 @@ export function DataTable<TData extends RotationExtended, TValue>({
},
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
Expand Down Expand Up @@ -582,4 +595,4 @@ export function DataTable<TData extends RotationExtended, TValue>({
</div>
</div>
)
}
}
57 changes: 32 additions & 25 deletions fdm-app/app/store/field-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,37 @@ interface FieldFilterState {
syncFarm: (farmId: string) => void
}

export const useFieldFilterStore = create<FieldFilterState>()(
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<FieldFilterState>()(
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",
)
43 changes: 43 additions & 0 deletions fdm-app/app/store/rotation-selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { create } from "zustand"
import { createJSONStorage, persist } from "zustand/middleware"
import { ssrSafeSessionJSONStorage } from "./storage"

interface RotationSelectionState {
farmId: string | null
selection: Record<string, Record<string, boolean>>
updateSelection: (
selection: Record<string, Record<string, boolean>>,
) => void
syncFarm: (farmId: string) => void
}

export const useRotationSelectionStore = create<RotationSelectionState>()(
persist(
(set, get) => ({
farmId: null,
selection: {},
updateSelection(
selection: Record<string, Record<string, boolean>>,
) {
const currentSelection = get().selection
const result: Record<string, Record<string, boolean>> = {}
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) {
set({ farmId, selection: {} })
}
},
}),
{
name: "rotation-selection-storage", // unique name
storage: createJSONStorage(() => ssrSafeSessionJSONStorage), // Use SSR-safe storage
},
),
)