6317dade9e
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>
94 lines
4.5 KiB
TypeScript
94 lines
4.5 KiB
TypeScript
"use client";
|
|
import { useMemo, useState } from "react";
|
|
import { buildBases, type BaseViewType, type BaseFilterScope } from "@/lib/tools/bases";
|
|
import { CodeOutput } from "@/components/tools/CodeOutput";
|
|
|
|
const VIEW_TYPES: BaseViewType[] = ["table", "cards", "list", "map"];
|
|
const SCOPES: { id: BaseFilterScope; label: string }[] = [
|
|
{ id: "none", label: "без фильтра" },
|
|
{ id: "folder", label: "по папке" },
|
|
{ id: "tag", label: "по тегу" },
|
|
{ id: "property", label: "по свойству (выражение)" },
|
|
];
|
|
|
|
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;
|
|
|
|
const SCOPE_PLACEHOLDER: Record<BaseFilterScope, string> = {
|
|
none: "",
|
|
folder: "Projects",
|
|
tag: "#проект",
|
|
property: 'status == "done"',
|
|
};
|
|
|
|
export function BasesForm() {
|
|
const [viewType, setViewType] = useState<BaseViewType>("table");
|
|
const [viewName, setViewName] = useState("Мои заметки");
|
|
const [propsText, setPropsText] = useState("file.name, status");
|
|
const [filterScope, setFilterScope] = useState<BaseFilterScope>("folder");
|
|
const [filterValue, setFilterValue] = useState("Projects");
|
|
const [sortBy, setSortBy] = useState("file.name");
|
|
const [sortDir, setSortDir] = useState<"ASC" | "DESC">("ASC");
|
|
const [groupBy, setGroupBy] = useState("");
|
|
const [limit, setLimit] = useState("");
|
|
|
|
const yaml = useMemo(
|
|
() => buildBases({
|
|
viewType, viewName,
|
|
properties: propsText.split(",").map((s) => s.trim()).filter(Boolean),
|
|
filterScope, filterValue,
|
|
sortBy, sortDir, groupBy,
|
|
limit: limit ? Number(limit) : undefined,
|
|
}),
|
|
[viewType, viewName, propsText, filterScope, filterValue, sortBy, sortDir, groupBy, limit],
|
|
);
|
|
|
|
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={viewType} onChange={(e) => setViewType(e.target.value as BaseViewType)}>
|
|
{VIEW_TYPES.map((t) => <option key={t} value={t}>{t}</option>)}
|
|
</select>
|
|
</label>
|
|
<label className="text-sm">Имя представления
|
|
<input className={field} style={fieldStyle} value={viewName} onChange={(e) => setViewName(e.target.value)} />
|
|
</label>
|
|
<label className="text-sm">Свойства / колонки (через запятую)
|
|
<input className={field} style={fieldStyle} placeholder="file.name, status, …" value={propsText} onChange={(e) => setPropsText(e.target.value)} />
|
|
</label>
|
|
|
|
<div className="flex items-end gap-2">
|
|
<label className="text-sm flex-1">Фильтр
|
|
<select className={field} style={fieldStyle} value={filterScope} onChange={(e) => setFilterScope(e.target.value as BaseFilterScope)}>
|
|
{SCOPES.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}
|
|
</select>
|
|
</label>
|
|
{filterScope !== "none" && (
|
|
<input className={field} style={fieldStyle} placeholder={SCOPE_PLACEHOLDER[filterScope]} value={filterValue} onChange={(e) => setFilterValue(e.target.value)} />
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-end gap-2">
|
|
<label className="text-sm flex-1">Сортировка (свойство)
|
|
<input className={field} style={fieldStyle} placeholder="не сортировать" value={sortBy} onChange={(e) => setSortBy(e.target.value)} />
|
|
</label>
|
|
<select className={field} style={{ ...fieldStyle, width: "auto" }} value={sortDir} onChange={(e) => setSortDir(e.target.value as "ASC" | "DESC")}>
|
|
<option value="ASC">ASC</option>
|
|
<option value="DESC">DESC</option>
|
|
</select>
|
|
</div>
|
|
<label className="text-sm">Группировка (свойство)
|
|
<input className={field} style={fieldStyle} placeholder="без группировки" value={groupBy} onChange={(e) => setGroupBy(e.target.value)} />
|
|
</label>
|
|
<label className="text-sm">Лимит
|
|
<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={yaml} tool="bases" label="Содержимое .base-файла" />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|