Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5173
"port": 5173,
"autoPort": true
}
]
}
21 changes: 11 additions & 10 deletions cypress/e2e/ocotillo/list-pages.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,25 @@ describe('Ocotillo List Pages', () => {
})

it('renders the project list with project rows and navigation targets', () => {
cy.visit('/ocotillo/well/projects')
cy.visit('/ocotillo/projects')
cy.wait('@getProjects')

cy.contains('h3', /^Projects$/).should('be.visible')
cy.get('input[aria-label="Filter rows on this page"]').should(
cy.get('input[aria-label="Search projects"]').should(
'have.attr',
'placeholder',
'Filter this page...'
'Search projects…'
)

cy.contains('[role="columnheader"]', 'Name').should('be.visible')
cy.contains('[role="columnheader"]', 'Description').should('be.visible')
cy.contains('[role="columnheader"]', 'Release Status').should('be.visible')
cy.contains('[role="columnheader"]', 'Type').should('be.visible')
// Native table semantics: the shadcn table renders th/tr rather than the
// DataGrid's explicit role attributes.
cy.contains('th', 'Name').should('be.visible')
cy.contains('th', 'Description').should('be.visible')
cy.contains('th', 'Release Status').should('be.visible')
cy.contains('th', 'Type').should('be.visible')

cy.contains('[role="row"]', projectAlpha.name).should('be.visible')
cy.contains('[role="row"]', projectBeta.name).should('be.visible')
cy.contains('tr', projectAlpha.name).should('be.visible')
cy.contains('tr', projectBeta.name).should('be.visible')
cy.contains(projectAlpha.description).should('be.visible')
})

Expand All @@ -68,7 +70,6 @@ describe('Ocotillo List Pages', () => {
cy.wait('@getContacts')

cy.contains('h3', /contacts & owners/i).should('be.visible')
cy.get('input[aria-label="Filter rows on this page"]').should('be.visible')

cy.contains('[role="columnheader"]', 'Name').should('be.visible')
cy.contains('[role="columnheader"]', 'Organization').should('be.visible')
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/ocotillo/show-pages.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('Ocotillo Show Pages', () => {
it('renders the project show page with details, map, and associated wells', () => {
interceptProjectShowFixtures()
cy.login()
cy.visit('/ocotillo/well/projects/show/10')
cy.visit('/ocotillo/projects/show/10')
cy.wait('@getProject')

cy.contains('h3', projectAlpha.name).should('be.visible')
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const listPageviewProps = (
pathname: string,
search: string
): Record<string, unknown> | undefined => {
if (pathname === '/ocotillo/well/projects') {
if (pathname === '/ocotillo/projects') {
return { page_template: 'projects_list' }
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1088,7 +1088,7 @@ const BREADCRUMB_RESOURCES: Record<
sample: { label: 'Samples', listHref: '/ocotillo/sample', resource: 'sample' },
projects: {
label: 'Projects',
listHref: '/ocotillo/well/projects',
listHref: '/ocotillo/projects',
resource: 'group',
},
}
Expand Down
120 changes: 42 additions & 78 deletions src/components/ListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
MuiEvent,
} from '@mui/x-data-grid'
import { settings } from '@/settings'
import React, { useMemo, useState } from 'react'
import React from 'react'
import { useNavigate } from 'react-router'
import {
CanAccess,
Expand All @@ -35,10 +35,12 @@ import {
} from '@/components/OcotilloPageHeader'

/**
* Standard layout for Ocotillo list pages: title, optional description, search
* bar, record count, and MUI DataGrid with shared toolbar. Wells and Projects
* both use this component; page files supply columns, dataGridProps, and any
* page-specific header buttons or row navigation.
* Standard layout for Ocotillo list pages: title, optional description, record
* count, and MUI DataGrid with shared toolbar. Wells and Projects both use this
* component; page files supply columns, dataGridProps, and any page-specific
* header buttons or row navigation. Lists that opt into searchMode="server"
* also get a search input; everything else filters via the DataGrid's Filters
* toolbar button.
*/

// Shows a dismissible chip for each active column filter.
Expand Down Expand Up @@ -238,9 +240,9 @@ export const ListPage: React.FC<ListPageProps> = ({
}

const navigate = useNavigate()
const [localQuickFilter, setLocalQuickFilter] = useState('')
const quickFilter =
searchMode === 'server' ? (searchValue ?? '') : localQuickFilter
// Only server-search lists get a search input; client-side filtering is the
// DataGrid's Filters toolbar button.
const isServerSearch = searchMode === 'server'

const { show } = useNavigation()
const { resource } = useResourceParams()
Expand Down Expand Up @@ -275,38 +277,7 @@ export const ListPage: React.FC<ListPageProps> = ({
const rowCount = dataGridProps.rowCount as number | undefined
const { rows: allRows, ...restDataGridProps } = dataGridProps

const getSearchableCellValue = (row: any, col: GridColDef<any>) => {
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 needle = quickFilter.toLowerCase().trim()

return allRows.filter((row: any) =>
columns.some((col) =>
getSearchableCellValue(row, col).toLowerCase().includes(needle)
)
)
}, [allRows, quickFilter, columns, searchMode])

const handleSearchChange = (value: string) => {
if (searchMode === 'server') {
onSearchChange?.(value)
} else {
setLocalQuickFilter(value)
}
}
const rows = allRows ?? []

const toolbarConfig = {
hideExport: restDataGridProps.paginationMode === 'server',
Expand Down Expand Up @@ -398,52 +369,45 @@ export const ListPage: React.FC<ListPageProps> = ({
>
{children}

{/* Search bar and record count sit outside the DataGrid to preserve input focus */}
{/* Server search bar and record count sit outside the DataGrid to preserve input focus */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: isServerSearch ? 'space-between' : 'flex-end',
px: 0,
pb: 1.5,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
border: 1,
borderColor: 'divider',
borderRadius: 1,
px: 1,
py: 0.25,
width: 400,
bgcolor: 'background.paper',
}}
>
<SearchIcon
sx={{ fontSize: 16, color: 'text.secondary', flexShrink: 0 }}
/>
<InputBase
value={quickFilter}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder={
searchPlaceholder ??
(searchMode === 'server'
? 'Search all records...'
: 'Filter this page...')
}
sx={{ fontSize: 14, flex: 1 }}
inputProps={{
'aria-label':
searchAriaLabel ??
(searchMode === 'server'
? 'Search all records'
: 'Filter rows on this page'),
{isServerSearch && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
border: 1,
borderColor: 'divider',
borderRadius: 1,
px: 1,
py: 0.25,
width: 400,
bgcolor: 'background.paper',
}}
/>
</Box>
>
<SearchIcon
sx={{ fontSize: 16, color: 'text.secondary', flexShrink: 0 }}
/>
<InputBase
value={searchValue ?? ''}
onChange={(e) => onSearchChange?.(e.target.value)}
placeholder={searchPlaceholder ?? 'Search all records...'}
sx={{ fontSize: 14, flex: 1 }}
inputProps={{
'aria-label': searchAriaLabel ?? 'Search all records',
}}
/>
</Box>
)}
{rowCount !== undefined && rowCount > 0 && (
<Typography variant="caption" color="text.secondary">
{rowCount.toLocaleString()} total records
Expand All @@ -454,7 +418,7 @@ export const ListPage: React.FC<ListPageProps> = ({
{/* Refine sets filterDebounceMs to 0 for server-side grids; restore MUI debounce so toolbar column filters keep input focus while typing. */}
<DataGrid
{...restDataGridProps}
rows={filteredRows}
rows={rows}
filterDebounceMs={
restDataGridProps.filterMode === 'server'
? 700
Expand Down
8 changes: 7 additions & 1 deletion src/components/MapComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ interface MapComponentProps {
popupContent?: any
setPopupContent?: any
onMouseMoveCallback?: any
/** Fires once the map is ready, for callers that fit bounds after mount. */
onLoad?: () => void
showDrawControls?: {
show: boolean
position?: ControlPosition
Expand Down Expand Up @@ -61,6 +63,7 @@ export const MapComponent = ({
popupContent,
setPopupContent,
onMouseMoveCallback,
onLoad,
setSelectionPolygons,
isLoading = false,
initialViewState,
Expand Down Expand Up @@ -279,7 +282,10 @@ export const MapComponent = ({
ref={mapRef}
initialViewState={initialViewState}
onClick={handleMouseClick}
onLoad={emitBoundsChange}
onLoad={() => {
emitBoundsChange()
onLoad?.()
}}
onMove={(evt) => {
setViewState(evt.viewState)
emitBoundsChange()
Expand Down
Loading
Loading