Two more inside-LMS generators under /tools, completing the 6-tool set: - Dataview: DQL builder (TABLE/LIST/TASK/CALENDAR, FROM/WHERE/SORT/LIMIT), output wrapped in a dataview code block - Bases: .base YAML builder (views, filters and:, order, sort, groupBy, limit); filter expressions emitted as single-quoted YAML scalars for safety Syntax verified against official Obsidian docs (research workflow). Audit fix: Bases sort entries use 'column:' (not 'property:') per documented working .base files; 'property:' is kept only for groupBy. 14 new tests (47 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
5.1 KiB
TypeScript
104 lines
5.1 KiB
TypeScript
"use client";
|
|
import { useMemo, useState } from "react";
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
import { buildDataview, type DataviewColumn, type DataviewQueryType } from "@/lib/tools/dataview";
|
|
import { CodeOutput } from "@/components/tools/CodeOutput";
|
|
|
|
const QUERY_TYPES: DataviewQueryType[] = ["TABLE", "LIST", "TASK", "CALENDAR"];
|
|
|
|
const field = "w-full p-2 text-sm";
|
|
const fieldStyle = { border: "1px solid var(--border)", borderRadius: "2px", backgroundColor: "var(--background)", color: "var(--foreground)" } as const;
|
|
|
|
export function DataviewForm() {
|
|
const [queryType, setQueryType] = useState<DataviewQueryType>("TABLE");
|
|
const [withoutId, setWithoutId] = useState(false);
|
|
const [columns, setColumns] = useState<DataviewColumn[]>([
|
|
{ expr: "file.name", alias: "Заметка" },
|
|
{ expr: "file.mtime", alias: "Изменено" },
|
|
]);
|
|
const [from, setFrom] = useState("#project");
|
|
const [where, setWhere] = useState("");
|
|
const [sortField, setSortField] = useState("file.mtime");
|
|
const [sortDir, setSortDir] = useState<"ASC" | "DESC">("DESC");
|
|
const [limit, setLimit] = useState("");
|
|
|
|
const query = useMemo(
|
|
() => buildDataview({
|
|
queryType, withoutId, columns,
|
|
from, where, sortField, sortDir,
|
|
limit: limit ? Number(limit) : undefined,
|
|
}),
|
|
[queryType, withoutId, columns, from, where, sortField, sortDir, limit],
|
|
);
|
|
|
|
const showColumns = queryType !== "TASK";
|
|
const colLabel = queryType === "CALENDAR" ? "поле даты" : queryType === "LIST" ? "поле (берётся первое)" : "колонки";
|
|
|
|
function updateCol(i: number, patch: Partial<DataviewColumn>) {
|
|
setColumns((prev) => prev.map((c, idx) => (idx === i ? { ...c, ...patch } : c)));
|
|
}
|
|
|
|
return (
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<div className="flex flex-col gap-3">
|
|
<label className="text-sm">Тип запроса
|
|
<select className={field} style={fieldStyle} value={queryType} onChange={(e) => setQueryType(e.target.value as DataviewQueryType)}>
|
|
{QUERY_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
|
|
</select>
|
|
</label>
|
|
|
|
{(queryType === "TABLE" || queryType === "LIST") && (
|
|
<label className="text-sm flex items-center gap-2">
|
|
<input type="checkbox" checked={withoutId} onChange={(e) => setWithoutId(e.target.checked)} />
|
|
WITHOUT ID (скрыть колонку-ссылку)
|
|
</label>
|
|
)}
|
|
|
|
{showColumns && (
|
|
<div className="flex flex-col gap-2">
|
|
<span className="text-sm">{colLabel}</span>
|
|
{columns.map((c, i) => (
|
|
<div key={i} className="flex items-center gap-2">
|
|
<input className={field} style={fieldStyle} placeholder="выражение (file.name)" value={c.expr} onChange={(e) => updateCol(i, { expr: e.target.value })} />
|
|
{queryType === "TABLE" && (
|
|
<input className={field} style={fieldStyle} placeholder="алиас" value={c.alias ?? ""} onChange={(e) => updateCol(i, { alias: e.target.value })} />
|
|
)}
|
|
<button type="button" onClick={() => setColumns((p) => p.filter((_, idx) => idx !== i))} aria-label="Удалить" className="btn-aubade inline-flex items-center justify-center p-2">
|
|
<Trash2 size={16} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
{queryType === "TABLE" && (
|
|
<button type="button" onClick={() => setColumns((p) => [...p, { expr: "" }])} className="btn-aubade inline-flex items-center justify-center gap-2 text-sm">
|
|
<Plus size={16} /> Колонка
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<label className="text-sm">FROM (источник)
|
|
<input className={field} style={fieldStyle} placeholder='#tag или "Папка" или [[Заметка]]' value={from} onChange={(e) => setFrom(e.target.value)} />
|
|
</label>
|
|
<label className="text-sm">WHERE (условие)
|
|
<input className={field} style={fieldStyle} placeholder="!completed" value={where} onChange={(e) => setWhere(e.target.value)} />
|
|
</label>
|
|
<div className="flex items-end gap-2">
|
|
<label className="text-sm flex-1">SORT (поле)
|
|
<input className={field} style={fieldStyle} value={sortField} onChange={(e) => setSortField(e.target.value)} />
|
|
</label>
|
|
<select className={field} style={{ ...fieldStyle, width: "auto" }} value={sortDir} onChange={(e) => setSortDir(e.target.value as "ASC" | "DESC")}>
|
|
<option value="DESC">DESC</option>
|
|
<option value="ASC">ASC</option>
|
|
</select>
|
|
</div>
|
|
<label className="text-sm">LIMIT
|
|
<input type="number" min={0} className={field} style={fieldStyle} placeholder="без лимита" value={limit} onChange={(e) => setLimit(e.target.value)} />
|
|
</label>
|
|
</div>
|
|
<div className="flex flex-col gap-4">
|
|
<CodeOutput code={query} tool="dataview" label="Запрос Dataview (вставь в заметку)" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|