Files
lms-sb/src/app/(student)/tools/bases/BasesForm.tsx
T
admins 6317dade9e Add Dataview and Bases generators (Toolbox Phase 2)
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>
2026-06-24 10:04:09 +05:00

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>
);
}