Add Evidence sidebar text filter for CE-WP-0010.
Filter input next to the Evidence caption; case-insensitive match on quote, commentary, and status when the query has three or more characters.
This commit is contained in:
parent
7f42564cf4
commit
664d7da100
1 changed files with 76 additions and 9 deletions
|
|
@ -104,6 +104,22 @@ function annotationOrderKey(annotation: Annotation | null): number {
|
||||||
return docOrderKey(annotation.selectors);
|
return docOrderKey(annotation.selectors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FILTER_MIN_CHARS = 3;
|
||||||
|
|
||||||
|
function matchesEvidenceFilter(
|
||||||
|
query: string,
|
||||||
|
quote: string,
|
||||||
|
commentary: string | undefined,
|
||||||
|
status: string,
|
||||||
|
): boolean {
|
||||||
|
const q = query.trim();
|
||||||
|
if (q.length < FILTER_MIN_CHARS) return true;
|
||||||
|
const needle = q.toLowerCase();
|
||||||
|
return [quote, commentary ?? "", status].some((p) =>
|
||||||
|
p.toLowerCase().includes(needle),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
const engine = useEngine();
|
const engine = useEngine();
|
||||||
const { document } = useActiveDocument();
|
const { document } = useActiveDocument();
|
||||||
|
|
@ -117,6 +133,8 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
|
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
|
||||||
const revision = useEngineRevision();
|
const revision = useEngineRevision();
|
||||||
|
|
||||||
|
const [filterQuery, setFilterQuery] = useState("");
|
||||||
|
|
||||||
// Build the sorted view-model: each item gets its order key + the
|
// Build the sorted view-model: each item gets its order key + the
|
||||||
// first annotation up-front so the render below doesn't have to
|
// first annotation up-front so the render below doesn't have to
|
||||||
// re-resolve them inside the map.
|
// re-resolve them inside the map.
|
||||||
|
|
@ -139,6 +157,17 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
revision,
|
revision,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const filteredItems = useMemo(() => {
|
||||||
|
return sortedItems.filter(({ item, annotation }) =>
|
||||||
|
matchesEvidenceFilter(
|
||||||
|
filterQuery,
|
||||||
|
annotation?.quote ?? "",
|
||||||
|
item.commentary,
|
||||||
|
item.status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [sortedItems, filterQuery]);
|
||||||
|
|
||||||
const pendingOrder = useMemo<number>(() => {
|
const pendingOrder = useMemo<number>(() => {
|
||||||
if (!pending) return Number.POSITIVE_INFINITY;
|
if (!pending) return Number.POSITIVE_INFINITY;
|
||||||
const c = pending.capture;
|
const c = pending.capture;
|
||||||
|
|
@ -148,15 +177,15 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
return c.startOffset;
|
return c.startOffset;
|
||||||
}, [pending]);
|
}, [pending]);
|
||||||
|
|
||||||
// Find the insert position for the pending capture form: first index
|
// Insert pending capture among the *unfiltered* document order so a
|
||||||
// whose order > pendingOrder, or sortedItems.length to append.
|
// narrow filter cannot hide the in-progress capture form.
|
||||||
const pendingInsertIndex = useMemo(() => {
|
const pendingInsertIndex = useMemo(() => {
|
||||||
if (!pending) return -1;
|
if (!pending) return -1;
|
||||||
for (let i = 0; i < sortedItems.length; i++) {
|
for (let i = 0; i < filteredItems.length; i++) {
|
||||||
if (sortedItems[i]!.order > pendingOrder) return i;
|
if (filteredItems[i]!.order > pendingOrder) return i;
|
||||||
}
|
}
|
||||||
return sortedItems.length;
|
return filteredItems.length;
|
||||||
}, [pending, pendingOrder, sortedItems]);
|
}, [pending, pendingOrder, filteredItems]);
|
||||||
|
|
||||||
const [openExportFor, setOpenExportFor] = useState<EvidenceItemId | null>(null);
|
const [openExportFor, setOpenExportFor] = useState<EvidenceItemId | null>(null);
|
||||||
const [editingId, setEditingId] = useState<EvidenceItemId | null>(null);
|
const [editingId, setEditingId] = useState<EvidenceItemId | null>(null);
|
||||||
|
|
@ -249,6 +278,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
|
aria-label="Evidence"
|
||||||
style={{
|
style={{
|
||||||
width: 320,
|
width: 320,
|
||||||
borderLeft: "1px solid #ddd",
|
borderLeft: "1px solid #ddd",
|
||||||
|
|
@ -259,7 +289,33 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h2 style={{ marginTop: 0, fontSize: 16 }}>Evidence</h2>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0, fontSize: 16, flex: "0 0 auto" }}>Evidence</h2>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={filterQuery}
|
||||||
|
onChange={(e) => setFilterQuery(e.target.value)}
|
||||||
|
placeholder="Filter…"
|
||||||
|
aria-label="Filter evidence"
|
||||||
|
data-testid="evidence-filter"
|
||||||
|
style={{
|
||||||
|
flex: "1 1 100px",
|
||||||
|
minWidth: 80,
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "4px 6px",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{!document && (
|
{!document && (
|
||||||
<p style={{ fontSize: 12, color: "#888" }}>No document open.</p>
|
<p style={{ fontSize: 12, color: "#888" }}>No document open.</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -269,8 +325,19 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
capture.
|
capture.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{document &&
|
||||||
|
sortedItems.length > 0 &&
|
||||||
|
filteredItems.length === 0 &&
|
||||||
|
!pending && (
|
||||||
|
<p
|
||||||
|
style={{ fontSize: 12, color: "#888" }}
|
||||||
|
data-testid="evidence-filter-empty"
|
||||||
|
>
|
||||||
|
No matches.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
||||||
{sortedItems.map((entry, i) => {
|
{filteredItems.map((entry, i) => {
|
||||||
const slotForCapture = pendingInsertIndex === i;
|
const slotForCapture = pendingInsertIndex === i;
|
||||||
return (
|
return (
|
||||||
<Fragment key={entry.item.id}>
|
<Fragment key={entry.item.id}>
|
||||||
|
|
@ -314,7 +381,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{pendingInsertIndex === sortedItems.length && (
|
{pendingInsertIndex === filteredItems.length && (
|
||||||
<li>
|
<li>
|
||||||
<InlineCaptureForm />
|
<InlineCaptureForm />
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue