From 0cb4111972e67c80735dcd166f1db9fa9782f4f1 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 10 Apr 2026 10:56:11 -0500 Subject: [PATCH 1/5] feat(thing/list): Add contacts to well --- src/interfaces/ocotillo/IWell.ts | 4 +++- src/pages/ocotillo/thing/list.tsx | 26 +++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/interfaces/ocotillo/IWell.ts b/src/interfaces/ocotillo/IWell.ts index 08fecb40..8555e9f1 100644 --- a/src/interfaces/ocotillo/IWell.ts +++ b/src/interfaces/ocotillo/IWell.ts @@ -1,4 +1,4 @@ -import type { IThing } from '@/interfaces/ocotillo' +import type { IContact, IThing } from '@/interfaces/ocotillo' import { z } from 'zod' import { zWellPurpose } from '@/generated/zod.gen' @@ -59,4 +59,6 @@ export interface IWell extends IThing { start_date: string | null end_date: string | null }[] + + contacts?: Partial[] | null } diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index ae273e71..ade022d5 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -21,7 +21,7 @@ export const SpringList: React.FC = () => { field: 'name', headerName: 'Name', type: 'string', - minWidth: 180, + minWidth: 100, flex: 1, }, { @@ -61,6 +61,11 @@ export const WellList: React.FC = () => { const { dataGridProps } = useDataGrid({ resource: 'thing/water-well', dataProviderName: 'ocotillo', + meta: { + params: { + include_contacts: true, + }, + }, pagination: { pageSize: 50 }, }) @@ -70,6 +75,7 @@ export const WellList: React.FC = () => { meta: { params: { thing_type: ['water well', 'geothermal well'], + include_contacts: true, }, }, }) @@ -80,7 +86,7 @@ export const WellList: React.FC = () => { field: 'name', headerName: 'Name', type: 'string', - minWidth: 160, + minWidth: 100, flex: 1, }, { @@ -108,7 +114,12 @@ export const WellList: React.FC = () => { flex: 1, sortable: false, valueGetter: (_: unknown, row: IWell) => - row.aquifers?.map((a) => a.aquifer_system).join(', ') ?? '', + row.aquifers + ?.map( + (a: { aquifer_system: string; aquifer_types: string[] }) => + a.aquifer_system + ) + .join(', ') ?? '', }, { field: 'release_status', @@ -138,6 +149,15 @@ export const WellList: React.FC = () => { width: 130, valueGetter: (v: string) => formatAppDate(v), }, + { + field: 'contacts', + headerName: 'Contacts', + minWidth: 180, + flex: 1, + sortable: false, + valueGetter: (_: unknown, row: IWell) => + row.contacts?.map((c) => c.name ?? '').join(', ') ?? '', + }, { field: 'well_completion_date', headerName: 'Completed', From f2b044ba9b688bbbcd2469fce35a8a1e3c3d7d86 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 10 Apr 2026 12:21:36 -0500 Subject: [PATCH 2/5] fix(contact/list): Fix broken link --- src/pages/ocotillo/contact/list.tsx | 32 ++++++++++++++++++-------- src/pages/ocotillo/thing/list.tsx | 35 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/pages/ocotillo/contact/list.tsx b/src/pages/ocotillo/contact/list.tsx index 8f2a8067..18c1d456 100644 --- a/src/pages/ocotillo/contact/list.tsx +++ b/src/pages/ocotillo/contact/list.tsx @@ -15,10 +15,7 @@ import { settings } from '@/settings' import { formatAppDateTime, formatPhone } from '@/utils' import { ListPage } from '@/components' import { useAccessCapabilities } from '@/hooks' -import { - filterConfidentialRows, - sanitizeContacts, -} from '@/utils' +import { filterConfidentialRows, sanitizeContacts } from '@/utils' export const ContactList: React.FC = () => { const { canViewConfidential } = useAccessCapabilities() @@ -126,7 +123,13 @@ export const ContactList: React.FC = () => { renderCell: (params) => { const things = params.row.things ?? [] return ( -
+
{things.map((thing, idx) => ( {idx > 0 && ', '} @@ -138,6 +141,7 @@ export const ContactList: React.FC = () => { id: thing.id, }, }} + onClick={(e) => e.stopPropagation()} > {thing.name} @@ -202,9 +206,15 @@ export const ContactList: React.FC = () => { /> {selectedContactId && ( <> - {canViewConfidential && } - {canViewConfidential && } - {canViewConfidential && } + {canViewConfidential && ( + + )} + {canViewConfidential && ( + + )} + {canViewConfidential && ( + + )} )} @@ -349,7 +359,11 @@ const InfoCard = ({ }) => ( - + ) diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index ade022d5..20f6ef07 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { useExport, useGo } from '@refinedev/core' +import { useExport, useGo, useLink, useNavigation } from '@refinedev/core' import { ExportButton, useDataGrid } from '@refinedev/mui' import { GridColDef } from '@mui/x-data-grid' import { Button } from '@mui/material' @@ -80,6 +80,9 @@ export const WellList: React.FC = () => { }, }) + const { create } = useNavigation() + const Link = useLink() + const columns = useMemo[]>( () => [ { @@ -157,6 +160,36 @@ export const WellList: React.FC = () => { sortable: false, valueGetter: (_: unknown, row: IWell) => row.contacts?.map((c) => c.name ?? '').join(', ') ?? '', + renderCell: (params) => { + const contacts = params.row.contacts ?? [] + return ( +
+ {contacts.map((contact, idx) => ( + + {idx > 0 && ', '} + e.stopPropagation()} + > + {contact.name} + + + ))} +
+ ) + }, }, { field: 'well_completion_date', From f4690e4013fcd95d6d8683d1681a83139f60b097 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 10 Apr 2026 12:26:05 -0500 Subject: [PATCH 3/5] fix(thing/list): Rm unused import --- src/pages/ocotillo/thing/list.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index de96c6f8..d179f45a 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from 'react' -import { useExport, useGo } from '@refinedev/core' +import { useExport, useGo, useLink } from '@refinedev/core' import { ExportButton, useDataGrid } from '@refinedev/mui' import { GridColDef, GridFilterModel } from '@mui/x-data-grid' import { captureEvent } from '@/analytics/posthog' @@ -97,7 +97,6 @@ export const WellList: React.FC = () => { }, }) - const { create } = useNavigation() const Link = useLink() const columns = useMemo[]>( @@ -297,7 +296,10 @@ export const WellList: React.FC = () => { ' construction depending on the local geology and intended use.' } columns={columns} - dataGridProps={{ ...dataGridProps, onFilterModelChange: handleFilterModelChange }} + dataGridProps={{ + ...dataGridProps, + onFilterModelChange: handleFilterModelChange, + }} getRowId={(row) => row.id} headerButtons={customHeaderButtons} /> From bf13b7421c2b6cb915dcb601195b9870745e7c87 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 10 Apr 2026 13:16:47 -0500 Subject: [PATCH 4/5] feat(ListPage): Impl client & server list page filtering --- src/components/ListPage.tsx | 147 ++++++++++++++++++++++++------ src/pages/ocotillo/thing/list.tsx | 17 +++- 2 files changed, 136 insertions(+), 28 deletions(-) diff --git a/src/components/ListPage.tsx b/src/components/ListPage.tsx index 51c580dc..839a59bd 100644 --- a/src/components/ListPage.tsx +++ b/src/components/ListPage.tsx @@ -13,14 +13,19 @@ import { GridFilterItem, useGridApiContext, useGridSelector, + GridColDef, } from '@mui/x-data-grid' import { settings } from '@/settings' import React, { useMemo, useState } from 'react' -import { CanAccess, useExport, useNavigation, useResourceParams } from '@refinedev/core' -import { Box, Chip, InputBase, Stack, Tooltip, Typography } from '@mui/material' +import { + CanAccess, + useExport, + useNavigation, + useResourceParams, +} from '@refinedev/core' +import { Box, Chip, InputBase, Stack, Typography } from '@mui/material' import SearchIcon from '@mui/icons-material/Search' - // Shows a dismissible chip for each active column filter. function ActiveFilterChips() { const apiRef = useGridApiContext() @@ -38,11 +43,23 @@ function ActiveFilterChips() { } return ( - + {activeFilters.map((filter) => { const column = columns[filter.field] const fieldLabel = column?.headerName ?? filter.field - const value = filter.value != null && filter.value !== '' ? ` ${filter.operator} "${filter.value}"` : ` ${filter.operator}` + const value = + filter.value != null && filter.value !== '' + ? ` ${filter.operator} "${filter.value}"` + : ` ${filter.operator}` return ( + - - - - + + + + @@ -77,17 +116,22 @@ function ListPageToolbar() { } type ListPageProps = { - title?: string | null - description?: string | null - columns: any + title?: string + description?: string + columns: GridColDef[] dataGridProps: any + getRowId?: (row: any) => string | number exportProps?: any - children?: any + children?: React.ReactNode onSelectionChange?: (selectionModel: any) => void - getRowId?: (row: any) => number - isLoading?: any + isLoading?: boolean headerButtons?: any disableRowClick?: boolean + + searchMode?: 'client' | 'server' + searchValue?: string + onSearchChange?: (value: string) => void + searchPlaceholder?: string } export const ListPage: React.FC = ({ @@ -102,12 +146,18 @@ export const ListPage: React.FC = ({ isLoading, headerButtons, disableRowClick = false, + searchMode = 'client', + searchValue, + onSearchChange, + searchPlaceholder, }) => { if (!exportProps) { exportProps = { pageSize: 1000 } } - const [quickFilter, setQuickFilter] = useState('') + const [localQuickFilter, setLocalQuickFilter] = useState('') + const quickFilter = + searchMode === 'server' ? (searchValue ?? '') : localQuickFilter const { show } = useNavigation() const { resource } = useResourceParams() @@ -133,16 +183,40 @@ export const ListPage: React.FC = ({ } const rowCount = dataGridProps.rowCount as number | undefined - const { rows: allRows, ...restDataGridProps } = dataGridProps + const getSearchableCellValue = (row: any, col: GridColDef) => { + const raw = row[col.field] + + if (raw == null) return '' + if (Array.isArray(raw)) return raw.map((v) => String(v)).join(', ') + if (typeof raw === 'object') return JSON.stringify(raw) + return String(raw) + } + const filteredRows = useMemo(() => { + if (searchMode === 'server') { + return allRows ?? [] + } + if (!quickFilter || !allRows) return allRows ?? [] - const lower = quickFilter.toLowerCase() + + const needle = quickFilter.toLowerCase().trim() + return allRows.filter((row: any) => - Object.values(row).some((val) => String(val ?? '').toLowerCase().includes(lower)) + columns.some((col) => + getSearchableCellValue(row, col).toLowerCase().includes(needle) + ) ) - }, [allRows, quickFilter]) + }, [allRows, quickFilter, columns, searchMode]) + + const handleSearchChange = (value: string) => { + if (searchMode === 'server') { + onSearchChange?.(value) + } else { + setLocalQuickFilter(value) + } + } return ( @@ -168,7 +242,12 @@ export const ListPage: React.FC = ({ breadcrumb={} wrapperProps={{ elevation: 0, - sx: { backgroundColor: 'background.wrapper', boxShadow: 'none', borderRadius: 1, padding: 0 }, + sx: { + backgroundColor: 'background.wrapper', + boxShadow: 'none', + borderRadius: 1, + padding: 0, + }, }} headerProps={{ sx: { @@ -209,13 +288,25 @@ export const ListPage: React.FC = ({ bgcolor: 'background.paper', }} > - + setQuickFilter(e.target.value)} - placeholder="Filter this page..." + onChange={(e) => handleSearchChange(e.target.value)} + placeholder={ + searchPlaceholder ?? + (searchMode === 'server' + ? 'Search all records...' + : 'Filter this page...') + } sx={{ fontSize: 14, flex: 1 }} - inputProps={{ 'aria-label': 'Filter rows on this page' }} + inputProps={{ + 'aria-label': + searchMode === 'server' + ? 'Search all records' + : 'Filter rows on this page', + }} /> {rowCount !== undefined && rowCount > 0 && ( @@ -245,7 +336,9 @@ export const ListPage: React.FC = ({ ? (params) => show(resource.name, params.id as string | number) : undefined } - loading={isLoading !== undefined ? isLoading : restDataGridProps.loading} + loading={ + isLoading !== undefined ? isLoading : restDataGridProps.loading + } columns={columns} sx={{ cursor: disableRowClick ? 'default' : 'pointer' }} /> diff --git a/src/pages/ocotillo/thing/list.tsx b/src/pages/ocotillo/thing/list.tsx index d179f45a..d846452e 100644 --- a/src/pages/ocotillo/thing/list.tsx +++ b/src/pages/ocotillo/thing/list.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useExport, useGo, useLink } from '@refinedev/core' import { ExportButton, useDataGrid } from '@refinedev/mui' import { GridColDef, GridFilterModel } from '@mui/x-data-grid' @@ -63,12 +63,24 @@ export const WellList: React.FC = () => { captureEvent('feature_used', { feature: 'wells_list' }) }, []) + const [searchInput, setSearchInput] = useState('') + const [search, setSearch] = useState('') + + useEffect(() => { + const timer = setTimeout(() => { + setSearch(searchInput.trim()) + }, 300) + + return () => clearTimeout(timer) + }, [searchInput]) + const { dataGridProps } = useDataGrid({ resource: 'thing/water-well', dataProviderName: 'ocotillo', meta: { params: { include_contacts: true, + ...(search ? { query: search } : {}), }, }, pagination: { pageSize: 50 }, @@ -302,6 +314,9 @@ export const WellList: React.FC = () => { }} getRowId={(row) => row.id} headerButtons={customHeaderButtons} + searchMode="server" + searchValue={searchInput} + onSearchChange={setSearchInput} /> ) } From 916e3833974a93fa438d547105f8476b1292ed68 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Fri, 10 Apr 2026 15:01:05 -0500 Subject: [PATCH 5/5] feat(ListPage): Improve UI layout --- src/components/ListPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ListPage.tsx b/src/components/ListPage.tsx index 839a59bd..0bf906de 100644 --- a/src/components/ListPage.tsx +++ b/src/components/ListPage.tsx @@ -270,8 +270,8 @@ export const ListPage: React.FC = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - px: 0.5, - pb: 0.5, + px: 0, + pb: 1.5, }} > = ({ borderRadius: 1, px: 1, py: 0.25, - width: 260, + width: 400, bgcolor: 'background.paper', }} >