mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
* feat(server): accept JSON Lines in the attachment text preview proxy The viewer now shows .jsonl / .ndjson files as a tree, so the text proxy's whitelist takes them too (extension and application/x-ndjson). The whitelist test gains the CSV / TSV / log / JSON Lines cases and says it is mirrored by the client-side table in packages/views/editor/utils/preview.test.ts. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(views): table, tree, numbered-line and viewport views in the attachment viewer MUL-7650, step 3 of MUL-7642. Fills in how the full-window viewer shows the file types agents produce most. - CSV / TSV (`table` kind): parsed to a table on the shared DataTable — pinned header, virtualized rows, resizable columns, a frozen row-number column. Clicking a header sorts ascending / descending / file order; numbers sort numerically, empty cells stay last, all-number columns align right. `;`-separated exports are sniffed. DataTable headers now carry aria-sort when the table sorts through TanStack's own state. - JSON / JSON Lines / YAML (`structured` kind): a collapsible tree with inline previews of collapsed records and paged long lists; Tree / Raw in the top bar. A file that does not parse falls back to the source and says why. YAML alias expansion stays capped by the parser. - Code, text and logs: numbered lines (the number is a pseudo-element, so copying skips it) with a sticky gutter, and a wrap toggle. Logs and plain text wrap by default, code does not; the reader's choice carries across the sequence. - HTML: Fit / Desktop 1440 / Tablet 768 / Phone 390. A device width lays the document out at that width and scales it down when the stage is narrower, with the size and scale shown under it; switching never remounts the iframe. A source toggle shows the HTML with line numbers. - Open in new tab works for every previewable kind: /{slug}/attachments/{id} /preview loads the record by id and renders the viewer's own top bar and stage (AttachmentPreviewStandalone), keeping the desktop scroll restore for HTML. - Whitelist: .jsonl / .ndjson join the text types on both sides, and the client test mirrors the server's case table. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]>
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
import { render } from "@testing-library/react";
|
|
import { describe, expect, it } from "vitest";
|
|
import { CodeBlockStatic } from "./code-block-static";
|
|
|
|
describe("CodeBlockStatic", () => {
|
|
it("uses the standalone rich-text-editor pre shape covered by code.css", () => {
|
|
const { container } = render(
|
|
<CodeBlockStatic language="bash" body="uv run --extra dev pytest -q" />,
|
|
);
|
|
|
|
const pre = container.querySelector("pre.rich-text-editor");
|
|
const code = container.querySelector("pre.rich-text-editor code");
|
|
|
|
expect(pre).not.toBeNull();
|
|
expect(code?.textContent).toBe("uv run --extra dev pytest -q");
|
|
});
|
|
|
|
it("renders unlabelled code without auto-detected highlight spans", () => {
|
|
const { container } = render(
|
|
<CodeBlockStatic language={undefined} body="const answer = 42;" />,
|
|
);
|
|
|
|
const code = container.querySelector("pre.rich-text-editor code");
|
|
expect(code?.textContent).toBe("const answer = 42;");
|
|
expect(code?.querySelector("span")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("CodeBlockStatic with lineNumbers", () => {
|
|
it("numbers every line, including blank ones", () => {
|
|
const { container } = render(
|
|
<CodeBlockStatic language="plaintext" body={"first\n\nthird\n"} lineNumbers />,
|
|
);
|
|
|
|
const lines = container.querySelectorAll(".code-line");
|
|
expect(Array.from(lines, (line) => line.getAttribute("data-line"))).toEqual([
|
|
"1",
|
|
"2",
|
|
"3",
|
|
]);
|
|
expect(Array.from(lines, (line) => line.textContent)).toEqual([
|
|
"first",
|
|
"",
|
|
"third",
|
|
]);
|
|
});
|
|
|
|
it("keeps a multi-line token highlighted on every line it spans", () => {
|
|
const { container } = render(
|
|
<CodeBlockStatic
|
|
language="javascript"
|
|
body={"/* one\ntwo */\nconst x = 1;"}
|
|
lineNumbers
|
|
/>,
|
|
);
|
|
|
|
const lines = container.querySelectorAll(".code-line");
|
|
expect(lines).toHaveLength(3);
|
|
expect(lines[0]?.querySelector(".hljs-comment")?.textContent).toBe("/* one");
|
|
expect(lines[1]?.querySelector(".hljs-comment")?.textContent).toBe("two */");
|
|
expect(lines[2]?.querySelector(".hljs-keyword")?.textContent).toBe("const");
|
|
});
|
|
|
|
it("sizes the gutter to the widest line number", () => {
|
|
const body = Array.from({ length: 120 }, (_, i) => `line ${i}`).join("\n");
|
|
const { container } = render(
|
|
<CodeBlockStatic language={undefined} body={body} lineNumbers />,
|
|
);
|
|
|
|
const pre = container.querySelector<HTMLElement>("pre.code-lines");
|
|
expect(pre?.style.getPropertyValue("--line-digits")).toBe("3");
|
|
});
|
|
|
|
it("wraps long lines only when asked", () => {
|
|
const { container, rerender } = render(
|
|
<CodeBlockStatic language={undefined} body="a" lineNumbers />,
|
|
);
|
|
expect(container.querySelector("pre")).not.toHaveClass("code-lines-wrap");
|
|
|
|
rerender(<CodeBlockStatic language={undefined} body="a" lineNumbers wrap />);
|
|
expect(container.querySelector("pre")).toHaveClass("code-lines-wrap");
|
|
});
|
|
|
|
it("escapes markup in the source", () => {
|
|
const { container } = render(
|
|
<CodeBlockStatic language={undefined} body="<script>alert(1)</script>" lineNumbers />,
|
|
);
|
|
|
|
expect(container.querySelector("script")).toBeNull();
|
|
expect(container.querySelector(".code-line")?.textContent).toBe(
|
|
"<script>alert(1)</script>",
|
|
);
|
|
});
|
|
});
|