| import { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
| import type { QueryOpenDetail } from "../apiContract"; |
| import { QUERY_OPEN_EVENT } from "../apiContract"; |
| import CustomerGrid from "../customer-grid/CustomerGrid"; |
| import { OverlayProvider } from "../customer-grid/OverlaySurface"; |
| import { gridScopeFor } from "../shell/dbFrame"; |
| import { databaseEntries } from "../shell/nav"; |
| import type { NavEntry } from "../shell/nav"; |
| import { bindingFor, deleteQuery, fetchQueries, QUERY_BINDING_EVENT } from "./queryApi"; |
| import type { QueryCitation, SavedQuery } from "./queryApi"; |
| import { QueryEmpty, queryGroups, QueryRail } from "./queryParts"; |
| import type { QueryGroup } from "./queryParts"; |
| import "./query.css"; |
|
|
| export interface QueryPageProps { |
| granted: NavEntry[]; |
| |
| |
| |
| |
| |
| |
| hostedRail?: boolean; |
| |
| refreshToken?: number; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| selectedId?: string; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export default function QueryPage({ granted, hostedRail, refreshToken, selectedId }: QueryPageProps) { |
| const [views, setViews] = useState<SavedQuery[]>([]); |
| const [citations, setCitations] = useState<QueryCitation[]>([]); |
| const [activeId, setActiveId] = useState(""); |
| const [loaded, setLoaded] = useState(false); |
| const entries = databaseEntries(granted); |
| const entryOf = (key: string) => entries.find((entry) => entry.key === key); |
| const known = views.filter((view) => entryOf(view.source.database)); |
| const active = known.find((view) => view.id === activeId) ?? known[0] ?? null; |
|
|
| const groups = useMemo<QueryGroup[]>(() => queryGroups(views, entries), [views, entries]); |
|
|
| const reload = useCallback(async () => { |
| const result = await fetchQueries(); |
| setLoaded(true); |
| if (result.ok) { |
| setViews(result.value.views); |
| setCitations(result.value.citations); |
| } |
| }, []); |
|
|
| useEffect(() => { void reload(); }, [reload]); |
|
|
| |
| useEffect(() => { if (refreshToken) void reload(); }, [refreshToken, reload]); |
|
|
| |
| |
| |
| |
| useEffect(() => { if (selectedId) setActiveId(selectedId); }, [selectedId]); |
|
|
| useEffect(() => { |
| if (known.length && !known.some((view) => view.id === activeId)) setActiveId(known[0].id); |
| }, [known, activeId]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const binding = useMemo( |
| () => (active ? bindingFor(active, citations) : undefined), [active, citations]); |
|
|
| useEffect(() => { |
| if (!binding) return; |
| |
| |
| |
| window.dispatchEvent(new CustomEvent(QUERY_BINDING_EVENT, { detail: binding })); |
| }, [binding]); |
|
|
| useEffect(() => { |
| const onOpen = (event: Event) => { |
| const qid = (event as CustomEvent<QueryOpenDetail>).detail?.qid; |
| if (!qid) return; |
| |
| |
| |
| |
| setActiveId(qid); |
| if (!views.some((view) => view.id === qid)) void reload(); |
| }; |
| window.addEventListener(QUERY_OPEN_EVENT, onOpen); |
| return () => window.removeEventListener(QUERY_OPEN_EVENT, onOpen); |
| }, [views, reload]); |
|
|
| |
| const upsert = useCallback((view: SavedQuery) => { |
| setViews((current) => [view, ...current.filter((row) => row.id !== view.id)]); |
| }, []); |
|
|
| const remove = useCallback(async (qid: string) => { |
| const result = await deleteQuery(qid); |
| if (!result.ok) return; |
| setViews((current) => current.filter((view) => view.id !== qid)); |
| setActiveId((current) => (current === qid ? "" : current)); |
| }, []); |
|
|
| if (!active) return <QueryEmpty loaded={loaded} unnamed={views.length - known.length} />; |
| |
| |
| |
| |
| |
| return ( |
| <div className="shell-db-frame"> |
| {/* |
| ββ WAVE 36 Β· W36-T67 (owner item 7) β THE UNIVERSAL DATABASE HEADER IS UNMOUNTED HERE, |
| AND ON THIS SURFACE IT WAS ALREADY A SECOND COPY OF SOMETHING VISIBLE. |
| β THE COMPONENT IS NOT NAMED ANYWHERE IN THIS FILE ANY MORE, DELIBERATELY. F's W36-T51 |
| deletes it and finds its last references by searching; a comment carrying the symbol is |
| a hit that costs somebody a read, and this very file already warns about prose becoming |
| its own marker one block below [[prose-that-becomes-its-own-marker]]. |
| |
| β ITEM 7'S OWN REASONING DOES NOT TRANSFER TO A QUERY, which is why this needed its own |
| ticket rather than following the database one. Owner: the header is *"a waste of white |
| space. User should be able to tell what database they are in just by seeing the first |
| Unique ID field."* A Query has no Unique ID column of its own to lean on. |
| β WHAT IT LEANS ON INSTEAD IS ALREADY ON SCREEN, and it is richer than the band that went: |
| `queryGroups` groups the rail BY SOURCE DATABASE and heads each group with that database's |
| label AND its icon β owner item 3 of 2026-08-15, *"each View correspond to the relevant |
| database"*. So `label={active.source.label}` was the same fact, painted twice, one of them |
| costing a full-width band. The `hostedRail` case loses nothing either: the Assistant hosts |
| the SAME `QueryRail` component, grouping included. |
| β THE ONE THING THAT WAS NOT DUPLICATED IS THE HEADING ITSELF. That header rendered an |
| `<h1>` its own comment called "the first time the work surface has NAMED itself"; deleting |
| it outright would leave this page with no heading in the accessibility tree at all. So the |
| name survives as an `h1` that costs NO vertical space, which is the half of the owner's |
| instruction this surface can honour exactly. `flex: 1 1 auto` on `.shell-grid-host` hands |
| the reclaimed band to the grid with no CSS change. |
| */} |
| <h1 className="lp-sr-only"> |
| {active.name || "Query"} on {active.source.label} |
| </h1> |
| <div className="shell-grid-host"> |
| <div className="qy-workspace"> |
| {hostedRail ? null : ( |
| <QueryRail groups={groups} activeId={active.id} onSelect={setActiveId} |
| onDelete={(id) => void remove(id)} onChanged={upsert} /> |
| )} |
| {/* β NO PROVENANCE ROW HERE (W35-T21, owner item 4 / R2). The "Sources" disclosure and |
| the snapshot line it opened are deleted; a Query view is a database view now, and a |
| database view carries no banner. The citations still travel β `binding` above is |
| built from this artefact AND the fetched citation list β and the reader still SEES |
| them in the chat, under the answer that used them (`AssistantPage::CitationLine`). |
| β Do NOT paste that builder call into a comment: `verify_grid_ux.py`'s NC mutates the |
| FIRST occurrence of it, so a second copy in prose keeps the scan green over a page |
| that stopped binding citations [[prose-that-becomes-its-own-marker]]. */} |
| <div className="qy-surface"> |
| <div className="qy-grid-host"> |
| <OverlayProvider> |
| <CustomerGrid key={gridScopeFor(active.source.database)} |
| scope={gridScopeFor(active.source.database)} queryBinding={binding} /> |
| </OverlayProvider> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
| |