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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const engine = useEngine();
|
||||
const { document } = useActiveDocument();
|
||||
|
|
@ -117,6 +133,8 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
const annotationUpdateTick = useEngineEventTick("AnnotationUpdated");
|
||||
const revision = useEngineRevision();
|
||||
|
||||
const [filterQuery, setFilterQuery] = useState("");
|
||||
|
||||
// Build the sorted view-model: each item gets its order key + the
|
||||
// first annotation up-front so the render below doesn't have to
|
||||
// re-resolve them inside the map.
|
||||
|
|
@ -139,6 +157,17 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
revision,
|
||||
]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return sortedItems.filter(({ item, annotation }) =>
|
||||
matchesEvidenceFilter(
|
||||
filterQuery,
|
||||
annotation?.quote ?? "",
|
||||
item.commentary,
|
||||
item.status,
|
||||
),
|
||||
);
|
||||
}, [sortedItems, filterQuery]);
|
||||
|
||||
const pendingOrder = useMemo<number>(() => {
|
||||
if (!pending) return Number.POSITIVE_INFINITY;
|
||||
const c = pending.capture;
|
||||
|
|
@ -148,15 +177,15 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
return c.startOffset;
|
||||
}, [pending]);
|
||||
|
||||
// Find the insert position for the pending capture form: first index
|
||||
// whose order > pendingOrder, or sortedItems.length to append.
|
||||
// Insert pending capture among the *unfiltered* document order so a
|
||||
// narrow filter cannot hide the in-progress capture form.
|
||||
const pendingInsertIndex = useMemo(() => {
|
||||
if (!pending) return -1;
|
||||
for (let i = 0; i < sortedItems.length; i++) {
|
||||
if (sortedItems[i]!.order > pendingOrder) return i;
|
||||
for (let i = 0; i < filteredItems.length; i++) {
|
||||
if (filteredItems[i]!.order > pendingOrder) return i;
|
||||
}
|
||||
return sortedItems.length;
|
||||
}, [pending, pendingOrder, sortedItems]);
|
||||
return filteredItems.length;
|
||||
}, [pending, pendingOrder, filteredItems]);
|
||||
|
||||
const [openExportFor, setOpenExportFor] = useState<EvidenceItemId | null>(null);
|
||||
const [editingId, setEditingId] = useState<EvidenceItemId | null>(null);
|
||||
|
|
@ -249,6 +278,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
|
||||
return (
|
||||
<aside
|
||||
aria-label="Evidence"
|
||||
style={{
|
||||
width: 320,
|
||||
borderLeft: "1px solid #ddd",
|
||||
|
|
@ -259,7 +289,33 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
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 && (
|
||||
<p style={{ fontSize: 12, color: "#888" }}>No document open.</p>
|
||||
)}
|
||||
|
|
@ -269,8 +325,19 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
capture.
|
||||
</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 }}>
|
||||
{sortedItems.map((entry, i) => {
|
||||
{filteredItems.map((entry, i) => {
|
||||
const slotForCapture = pendingInsertIndex === i;
|
||||
return (
|
||||
<Fragment key={entry.item.id}>
|
||||
|
|
@ -314,7 +381,7 @@ export function EvidenceSidebar(props: EvidenceSidebarProps) {
|
|||
</Fragment>
|
||||
);
|
||||
})}
|
||||
{pendingInsertIndex === sortedItems.length && (
|
||||
{pendingInsertIndex === filteredItems.length && (
|
||||
<li>
|
||||
<InlineCaptureForm />
|
||||
</li>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue