Spaces:
Runtime error
Runtime error
File size: 4,102 Bytes
34b0226 070c931 34b0226 070c931 34b0226 070c931 34b0226 070c931 34b0226 070c931 34b0226 070c931 34b0226 db8dd62 070c931 db8dd62 070c931 34b0226 070c931 db8dd62 070c931 34b0226 070c931 db8dd62 070c931 34b0226 070c931 db8dd62 070c931 34b0226 070c931 34b0226 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | import { useEffect, useMemo, useState } from "react";
import { docsApi } from "../api/endpoints";
import type { Doc } from "../api/types";
import { formatDate, Loading } from "./ui";
/** Searchable checklist of posts. `single` turns it into a radio list. Pass `docs` to pick from those (a notebook's
* posts) instead of the whole archive. Locked posts (not indexed on this plan) are shown but cannot be picked. */
export function DocPicker({
selected,
onChange,
single = false,
exclude = [],
max,
docs: given,
count = true,
}: {
selected: string[];
onChange: (ids: string[]) => void;
single?: boolean;
exclude?: string[];
max?: number;
docs?: Doc[];
/** Show "N selected" next to the controls (off when the caller says it elsewhere). */
count?: boolean;
}) {
const [fetched, setFetched] = useState<Doc[] | null>(null);
const [q, setQ] = useState("");
const docs = given ?? fetched;
useEffect(() => {
if (!given) docsApi.list({ limit: 5000 }).then((p) => setFetched(p.items));
}, [given]);
const visible = useMemo(() => {
const skip = new Set(exclude);
const needle = q.trim().toLowerCase();
return (docs ?? []).filter(
(d) => !skip.has(d.id) && (!needle || d.title.toLowerCase().includes(needle) || (d.source_title ?? "").toLowerCase().includes(needle)),
);
}, [docs, q, exclude]);
// The tick-all box: ticked only while every pickable post in view is ticked, or with a `max`, exactly the first `max`
// of them. Ticking it picks those, unticking it clears them.
const pickable = visible.filter((d) => !d.locked);
const full = !!max && selected.length >= max;
const firstFew = max ? pickable.slice(0, max).map((d) => d.id) : null;
const allOn = pickable.length > 0 && (firstFew
? selected.length === firstFew.length && firstFew.every((id) => selected.includes(id))
: pickable.every((d) => selected.includes(d.id)));
if (!docs) return <Loading label="Loading posts" />;
if (docs.length === 0) return <p className="muted">No posts yet. Connect a source first.</p>;
const selectAll = () => {
if (firstFew) return onChange(firstFew);
onChange(Array.from(new Set([...selected, ...pickable.map((d) => d.id)])));
};
const toggle = (id: string) => {
if (single) return onChange([id]);
if (selected.includes(id)) return onChange(selected.filter((x) => x !== id));
if (max && selected.length >= max) return;
onChange([...selected, id]);
};
return (
<div className="picker">
<div className="picker-bar">
<input className="input input-sm" placeholder="Search posts" value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search posts" />
{!single && (
<div className="picker-actions mono muted">
<label className="picker-all">
<input type="checkbox" checked={allOn} disabled={pickable.length === 0}
onChange={() => (allOn ? onChange(selected.filter((id) => !pickable.some((d) => d.id === id))) : selectAll())} />
{max && pickable.length > max ? `Select first ${max}` : "Select All"}
</label>
{count && <span>{selected.length} selected{max ? ` of ${max}` : ""}</span>}
</div>
)}
</div>
<ul className="picker-list">
{visible.map((d) => (
<li key={d.id}>
<label className={`picker-row${selected.includes(d.id) ? " on" : ""}${d.locked ? " locked" : ""}`}>
<input type={single ? "radio" : "checkbox"} name="doc-picker" checked={selected.includes(d.id)} disabled={d.locked || (!single && full && !selected.includes(d.id))}
onChange={() => toggle(d.id)} />
<span className="picker-title">{d.title}</span>
<span className="mono muted picker-meta">
{formatDate(d.published_at)} · {d.locked ? "Not indexed on your plan" : `${d.words} words`}
</span>
</label>
</li>
))}
{visible.length === 0 && <li className="muted">No posts match.</li>}
</ul>
</div>
);
}
|