From 25a3fd019b42272a3d43a88be1d860023036cf90 Mon Sep 17 00:00:00 2001 From: Jeremy Zilar Date: Sat, 13 Jun 2026 14:13:28 -0400 Subject: [PATCH 01/14] Add Projects list under Wells with filtered wells view. Includes project well counts, URL-based project filtering on the wells grid, indigo primary tokens, and filter pill styling. --- src/components/AppShell.tsx | 209 +++++++++++++++--- src/components/ListPage.tsx | 57 ++++- src/components/NewVersionBanner.tsx | 2 +- src/components/ui/badge.tsx | 53 +++++ src/config/navigation.ts | 14 ++ src/hooks/index.ts | 1 + src/hooks/useListPageDataGridAnalytics.ts | 87 ++++++++ src/index.css | 151 +++++++------ src/interfaces/ocotillo/IGroup.ts | 1 + src/pages/ocotillo/contact/list.tsx | 36 +-- src/pages/ocotillo/thing/index.tsx | 1 + src/pages/ocotillo/thing/list.tsx | 201 +++++++++++------ .../ocotillo/thing/well-projects/list.tsx | 82 +++++++ src/resources/ocotillo.tsx | 13 +- src/routes/ocotillo.tsx | 9 + src/test/utils/accessControl.test.ts | 4 + src/theme.ts | 18 +- src/utils/accessControl.ts | 6 + src/well-list/wellListColumnLabels.ts | 1 + 19 files changed, 743 insertions(+), 203 deletions(-) create mode 100644 src/components/ui/badge.tsx create mode 100644 src/hooks/useListPageDataGridAnalytics.ts create mode 100644 src/pages/ocotillo/thing/well-projects/list.tsx diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 6f6857f5..170caec4 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -162,15 +162,26 @@ function isActive(pathname: string, href: string) { return pathname === href || pathname.startsWith(href + '/') } +function collectNavHrefs(items: NavItem[]): string[] { + return items.flatMap((item) => { + const hrefs = item.href ? [item.href] : [] + if (item.children?.length) { + hrefs.push(...collectNavHrefs(item.children)) + } + return hrefs + }) +} + /** * Returns the href of the most specific nav item that matches the current * pathname. Prevents a shallow route (e.g. /ocotillo/well) from staying * active when a deeper route (e.g. /ocotillo/well/batch-export) is open. */ function activeHref(pathname: string): string | null { - const allHrefs = [...PRIMARY_NAV, ...RESOURCE_NAV] - .map((item) => item.href) - .filter(Boolean) as string[] + const allHrefs = [ + ...collectNavHrefs(PRIMARY_NAV), + ...collectNavHrefs(RESOURCE_NAV), + ] const matches = allHrefs.filter((h) => isActive(pathname, h)) if (matches.length === 0) return null return matches.reduce((a, b) => (a.length >= b.length ? a : b)) @@ -195,6 +206,120 @@ function SidebarBrand() { ) } +function isNavSectionActive(pathname: string, href: string) { + return pathname === href || pathname.startsWith(`${href}/`) +} + +function ResourceNavItem({ + item, + pathname, + canSeeNavItem, +}: { + item: NavItem + pathname: string + canSeeNavItem: (itemRoles: NavItem['roles']) => boolean +}) { + const { label, href, icon: Icon, resource, roles, children } = item + if (!canSeeNavItem(roles)) return null + + const visibleChildren = + children?.filter((child) => canSeeNavItem(child.roles)) ?? [] + const hasChildren = visibleChildren.length > 0 + const currentActiveHref = activeHref(pathname) + const sectionActive = + hasChildren && href != null && isNavSectionActive(pathname, href) + const [open, setOpen] = useState(sectionActive) + const isOpen = sectionActive || open + + useEffect(() => { + setOpen(sectionActive) + }, [sectionActive]) + + const handleOpenChange = (next: boolean) => { + if (!sectionActive) setOpen(next) + } + + if (!hasChildren) { + return ( + + + + + + {label} + {roles && !roles.includes(AmpRole.Viewer) && ( + + )} + + + + + ) + } + + const groupClass = `group/nav-${resource?.replace(/\./g, '-') ?? label}` + + return ( + + + + + + + + {label} + + + + + + + {visibleChildren.map((child) => { + const ChildIcon = child.icon + return ( + + + + + + {child.label} + + + + + ) + })} + + + + + + ) +} + function AppSidebar() { const location = useLocation() const { state } = useSidebar() @@ -280,32 +405,14 @@ function AppSidebar() { - {RESOURCE_NAV.map(({ label, href, icon: Icon, resource, roles }) => { - if (!canSeeNavItem(roles)) return null - - return ( - - - - - - {label} - {roles && !roles.includes(AmpRole.Viewer) && ( - - )} - - - - - ) - })} + {RESOURCE_NAV.map((item) => ( + + ))} {/* ── TEMPORARY: Example section — hidden until editing-tools is ready ── */} {/* */} @@ -855,12 +962,52 @@ const BREADCRUMB_RESOURCES: Record< sample: { label: 'Samples', listHref: '/ocotillo/sample', resource: 'sample' }, } +// Nested list pages shown as Parent > Current in the header bar +const NESTED_LIST_BREADCRUMBS: Record< + string, + { parentLabel: string; parentHref: string; label: string } +> = { + '/ocotillo/well/projects': { + parentLabel: 'Wells', + parentHref: '/ocotillo/well', + label: 'Projects', + }, +} + // Pattern: ///show/ or ///edit/ const DETAIL_PATTERN = /\/([a-z0-9-]+)\/(show|edit)\/([^/]+)$/ +function NestedListBreadcrumb({ + parentLabel, + parentHref, + label, +}: { + parentLabel: string + parentHref: string + label: string +}) { + return ( + + ) +} + function HeaderBreadcrumb() { const location = useLocation() + const nestedList = NESTED_LIST_BREADCRUMBS[location.pathname] + if (nestedList) { + return + } + const routeMatch = location.pathname.match(DETAIL_PATTERN) const slug = routeMatch?.[1] ?? '' const id = routeMatch?.[3] ?? '' @@ -880,7 +1027,7 @@ function HeaderBreadcrumb() { const recordLabel = recordName ?? `#${id}` return ( -