mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Apply the approved wording table: labels stay short, explanations are one full sentence about the user's traffic, and detail moves behind an info icon on the first-run card, the body banners, the Settings Helper row and the rejected card. One word per thing: traffic and flows, recorded, truncated, domains, searches, the CLI, Retry. Backend messages that reach the window change with them, with the tests and harness fixtures that assert on the old text. The two working files for the pass are removed from the branch. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import type { Completion } from "@codemirror/autocomplete";
|
|
import type { QueryDiagnostic, SavedQuery } from "@yaakapp-internal/proxy-lib";
|
|
import type { QueryProblem } from "@yaakapp-internal/ui/editor";
|
|
import { asOneTerm } from "./query";
|
|
import { rpc } from "./rpc";
|
|
|
|
/** How long values read for completion are offered before they are read again */
|
|
const VALUES_MS = 10_000;
|
|
|
|
const seenValues = new Map<string, { values: string[]; at: number }>();
|
|
const reading = new Map<string, Promise<string[]>>();
|
|
|
|
/**
|
|
* The values the proxy has seen for field paths, for completing them: read from the database
|
|
* the first time each is asked for, and again once they are a few seconds old, so the list
|
|
* keeps up with the traffic without a read per keystroke.
|
|
*/
|
|
export async function loadSeenValues(paths: string[]): Promise<Record<string, string[]>> {
|
|
const now = Date.now();
|
|
const entries = await Promise.all(
|
|
paths.map(async (path): Promise<[string, string[]]> => {
|
|
const cached = seenValues.get(path);
|
|
if (cached != null && now - cached.at < VALUES_MS) return [path, cached.values];
|
|
let read = reading.get(path);
|
|
if (read == null) {
|
|
read = rpc("query_values", { path })
|
|
.then((values) => {
|
|
seenValues.set(path, { values, at: Date.now() });
|
|
return values;
|
|
})
|
|
.catch(() => cached?.values ?? [])
|
|
.finally(() => reading.delete(path));
|
|
reading.set(path, read);
|
|
}
|
|
return [path, await read];
|
|
}),
|
|
);
|
|
return Object.fromEntries(entries);
|
|
}
|
|
|
|
const PROBLEM_LABELS: Record<QueryDiagnostic["kind"], string> = {
|
|
unknownField: "Unknown field",
|
|
invalidValue: "Invalid value",
|
|
notYet: "Not supported",
|
|
};
|
|
|
|
/** The terms of a search the backend cannot run, as the box underlines them */
|
|
export function problemsOf(diagnostics: QueryDiagnostic[]): QueryProblem[] {
|
|
return diagnostics.map((d) => ({
|
|
from: d.start,
|
|
to: d.end,
|
|
label: PROBLEM_LABELS[d.kind],
|
|
quiet: d.kind === "notYet",
|
|
}));
|
|
}
|
|
|
|
export async function searchProblems(text: string): Promise<QueryProblem[]> {
|
|
if (text.trim() === "") return [];
|
|
return problemsOf(await rpc("validate_query", { query: text }));
|
|
}
|
|
|
|
/**
|
|
* Saved searches whose names hold the word being typed, each replacing it with its search. One
|
|
* with `OR` at its top goes in parentheses, so it stays one group beside the box's other terms.
|
|
*/
|
|
export function savedCompletions(saved: SavedQuery[], typed: string): Completion[] {
|
|
const needle = typed.toLowerCase();
|
|
return saved
|
|
.filter((s) => s.name.toLowerCase().includes(needle))
|
|
.map((s) => {
|
|
const insert = asOneTerm(s.query);
|
|
return {
|
|
label: s.name,
|
|
detail: s.query,
|
|
type: "text",
|
|
apply: insert,
|
|
};
|
|
});
|
|
}
|