diff --git a/apps/roam/src/components/results-view/CustomView.tsx b/apps/roam/src/components/results-view/CustomView.tsx new file mode 100644 index 000000000..56d9abc02 --- /dev/null +++ b/apps/roam/src/components/results-view/CustomView.tsx @@ -0,0 +1,31 @@ +import React, { useMemo } from "react"; +import { compileTemplate, sanitizeHtml } from "~/utils/compileTemplate"; +import type { Result } from "~/utils/types"; + +export const DEFAULT_TEMPLATE = ``; + +type CustomViewProps = { + results: Result[]; + template?: string; +}; + +const CustomView = ({ results, template = DEFAULT_TEMPLATE }: CustomViewProps) => { + const html = useMemo(() => { + const compiled = compileTemplate({ template, results }); + return sanitizeHtml({ html: compiled }); + }, [results, template]); + + return ( +
+ ); +}; + +export default CustomView; + diff --git a/apps/roam/src/components/results-view/ResultsView.tsx b/apps/roam/src/components/results-view/ResultsView.tsx index dc2c9c091..e122dc616 100644 --- a/apps/roam/src/components/results-view/ResultsView.tsx +++ b/apps/roam/src/components/results-view/ResultsView.tsx @@ -37,6 +37,7 @@ import getUids from "roamjs-components/dom/getUids"; import Charts from "./Charts"; import Timeline from "./Timeline"; import Kanban from "./Kanban"; +import CustomView, { DEFAULT_TEMPLATE } from "./CustomView"; import MenuItemSelect from "roamjs-components/components/MenuItemSelect"; import type { RoamBasicNode } from "roamjs-components/types/native"; import { render as renderToast } from "roamjs-components/components/Toast"; @@ -62,7 +63,11 @@ const EMBED_FOLD_VALUES = ["default", "open", "closed"]; type EnglishQueryPart = { text: string; clickId?: string }; -const QueryUsed = ({ parentUid }: { parentUid: string }) => { +const QueryUsed = ({ + parentUid, +}: { + parentUid: string; +}) => { const { datalogQuery, englishQuery } = useMemo(() => { const args = parseQuery(parentUid); const { query: datalogQuery } = getDatalogQuery(args); @@ -225,6 +230,11 @@ const SUPPORTED_LAYOUTS = [ { key: "legend", label: "Show Legend", options: ["No", "Yes"] }, ], }, + { + id: "custom", + icon: "code-block", + settings: [], + }, ] as const; const settingsById = Object.fromEntries( SUPPORTED_LAYOUTS.map((l) => [l.id, l.settings]), @@ -452,6 +462,54 @@ const ResultsView: ResultsViewComponent = ({ () => views.filter((view) => view.mode !== "hidden").length, [views], ); + const customViewPrompt = useMemo(() => { + const selectionKeys = Array.from( + new Set( + columns + .map((c) => c.key.trim()) + .filter(Boolean), + ), + ); + const keyLines = selectionKeys.map((k) => `- ${k}`).join("\n"); + const exampleKey = selectionKeys.find((k) => k !== "text") || "text"; + const exampleTemplate = ``; + const groupByExampleTemplate = ` +{{#each results}} + + + + +{{/each}} +
{{resultIfChanged.${exampleKey}}}{{result.text}}
`; + + return `Create a Custom HTML Layout for a query result renderer. + +Requirements: +- Render with {{#each results}}...{{/each}} +- Use interpolations like {{result.key}} +- You can suppress repeated values with {{resultIfChanged.key}} or {{#ifChanged result.key}}...{{/ifChanged}} +- Do not use JavaScript, window access, or side effects +- Prefer simple, minimal CSS (avoid complex styling) +- Emulate a "group by" row pattern: when looping each result, if "{enter field}" matches the previous row's value, do not render that field again for the current row. + +Available result keys: +${keyLines || "- text"} + +Default template example: +${DEFAULT_TEMPLATE} + +Selection-specific template example: +${exampleTemplate} + +Group-by style example (hide repeated values): +${groupByExampleTemplate} + +Custom view description:`; + }, [columns]); return (
); })} + {layoutMode === "custom" && ( +