mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95d49d2adf | ||
|
|
f5228ec6d0 | ||
|
|
5fff9a1dec | ||
|
|
560002e0c6 |
No files matched your search
@@ -1259,3 +1259,18 @@
|
||||
flex: none;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
/* Wrap large values instead of laying megabytes out as one line. */
|
||||
.largeCellEditor {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: min(60vh, 480px);
|
||||
padding: 8px;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
resize: vertical;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { commands } from "@/platform/ipc";
|
||||
import { ExportDialog } from "./ExportDialog";
|
||||
import { cellPreview } from "./cellPreview";
|
||||
|
||||
vi.mock("./store", () => ({ closeDialog: vi.fn() }));
|
||||
vi.mock("@/workbench/contributions", () => ({ notifications: () => ({ notify: vi.fn() }) }));
|
||||
vi.mock("@tauri-apps/api/path", () => ({ homeDir: async () => "/home/test" }));
|
||||
|
||||
describe("large value export", () => {
|
||||
it("bounds preview values while exporting every row in full", async () => {
|
||||
const full = JSON.stringify({ text: "\u4e2d text ".repeat(200_000), tail: "complete" });
|
||||
const dbExportFile = vi.spyOn(commands, "dbExportFile").mockResolvedValue({ status: "ok", data: null });
|
||||
const rows = [[full], ["short"], [null]];
|
||||
render(
|
||||
<ExportDialog
|
||||
data={{ columns: [{ name: "payload", type: "jsonb" }], rows, table: "big", driver: "postgres" }}
|
||||
source="public.big"
|
||||
initialFormat="json"
|
||||
/>,
|
||||
);
|
||||
const preview = screen.getByLabelText("Export preview");
|
||||
expect(JSON.parse(preview.textContent!)).toEqual([
|
||||
{ payload: cellPreview(full) },
|
||||
{ payload: "short" },
|
||||
{ payload: null },
|
||||
]);
|
||||
await waitFor(() => expect(screen.getByLabelText("Output file")).toHaveValue("/home/test/big.json"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Export to File" }));
|
||||
await waitFor(() => expect(dbExportFile).toHaveBeenCalled());
|
||||
const [path, contents] = dbExportFile.mock.calls[0]!;
|
||||
expect(path).toBe("/home/test/big.json");
|
||||
expect(JSON.parse(contents)).toEqual(rows.map(([payload]) => ({ payload })));
|
||||
dbExportFile.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { CodePreview } from "./CodePreview";
|
||||
import { closeDialog } from "./store";
|
||||
import styles from "./Database.module.css";
|
||||
import { cellPreview } from "./cellPreview";
|
||||
|
||||
const PREVIEW_ROWS = 100;
|
||||
/** Formats with a tabular layout (the options apply). */
|
||||
@@ -59,7 +60,15 @@ export function ExportDialog({
|
||||
const tabular = TABULAR.has(format);
|
||||
const effective = tabular ? options : DEFAULT_EXPORT_OPTIONS;
|
||||
const preview = useMemo(
|
||||
() => exportData(format, { ...data, rows: data.rows.slice(0, PREVIEW_ROWS) }, effective),
|
||||
() =>
|
||||
exportData(
|
||||
format,
|
||||
{
|
||||
...data,
|
||||
rows: data.rows.slice(0, PREVIEW_ROWS).map((row) => row.map((v) => (v === null ? null : cellPreview(v)))),
|
||||
},
|
||||
effective,
|
||||
),
|
||||
[format, data, effective],
|
||||
);
|
||||
const text = () => exportData(format, data, effective);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { registerActions } from "@/platform/actions";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { Grid } from "./Grid";
|
||||
import { CELL_PREVIEW_LIMIT, cellPreview } from "./cellPreview";
|
||||
import { exportData } from "./export";
|
||||
|
||||
vi.mock("@tanstack/react-virtual", () => ({
|
||||
useVirtualizer: ({ count }: { count: number }) => ({
|
||||
@@ -72,3 +74,59 @@ describe("database grid", () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("large database cells", () => {
|
||||
const full = JSON.stringify({ text: "\u8bf4\u660e text \u6df7\u5408 ".repeat(200_000), tail: "complete" });
|
||||
const columns = [{ name: "payload", type: "jsonb" }];
|
||||
|
||||
it("preserves short values and truncates only beyond the preview limit", () => {
|
||||
for (const value of ["", "hello", "\u4e2d".repeat(CELL_PREVIEW_LIMIT)]) expect(cellPreview(value)).toBe(value);
|
||||
expect(cellPreview(full)).toBe(full.slice(0, CELL_PREVIEW_LIMIT) + "\u2026");
|
||||
expect(cellPreview("a".repeat(999) + "\ud83d\ude00tail")).toBe("a".repeat(999) + "\u2026");
|
||||
});
|
||||
|
||||
it("bounds rendered text while copying and exporting the complete value", () => {
|
||||
const writeText = vi.fn();
|
||||
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
|
||||
render(<Grid columns={columns} rowCount={1} value={() => full} />);
|
||||
const cell = screen.getByRole("gridcell");
|
||||
expect(cell.textContent).toBe(cellPreview(full));
|
||||
expect(cell.title).toBe(full.slice(0, 2000));
|
||||
fireEvent.mouseDown(cell, { button: 0 });
|
||||
fireEvent.keyDown(screen.getByRole("grid"), { key: "c", ctrlKey: true });
|
||||
expect(writeText).toHaveBeenCalledWith(full);
|
||||
expect(JSON.parse(exportData("json", { columns, rows: [[full]], table: "big", driver: "postgres" }))).toEqual([
|
||||
{ payload: full },
|
||||
]);
|
||||
});
|
||||
|
||||
it("edits the full large value in a dialog and supports cancel and apply", () => {
|
||||
const onEdit = vi.fn();
|
||||
render(<Grid columns={columns} rowCount={1} value={() => full} editable onEdit={onEdit} />);
|
||||
fireEvent.doubleClick(screen.getByRole("gridcell"));
|
||||
expect(screen.getByRole("dialog")).toBeTruthy();
|
||||
const editor = screen.getByRole("textbox") as HTMLTextAreaElement;
|
||||
expect(editor.tagName).toBe("TEXTAREA");
|
||||
expect(editor.value).toBe(full);
|
||||
fireEvent.change(editor, { target: { value: full + "changed" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onEdit).not.toHaveBeenCalled();
|
||||
fireEvent.doubleClick(screen.getByRole("gridcell"));
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: full + "changed" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
|
||||
expect(onEdit).toHaveBeenCalledWith(0, 0, full + "changed");
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps short values in the inline editor and preserves null display", () => {
|
||||
const onEdit = vi.fn();
|
||||
const { rerender } = render(<Grid columns={columns} rowCount={1} value={() => "short"} editable onEdit={onEdit} />);
|
||||
fireEvent.doubleClick(screen.getByRole("gridcell"));
|
||||
expect(screen.getByRole("textbox").tagName).toBe("INPUT");
|
||||
fireEvent.change(screen.getByRole("textbox"), { target: { value: "updated" } });
|
||||
fireEvent.keyDown(screen.getByRole("textbox"), { key: "Enter" });
|
||||
expect(onEdit).toHaveBeenCalledWith(0, 0, "updated");
|
||||
rerender(<Grid columns={columns} rowCount={1} value={() => null} />);
|
||||
expect(screen.getByRole("gridcell").textContent).toBe("<null>");
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,11 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ClipboardEvent, type KeyboardEvent, type MouseEvent } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import clsx from "clsx";
|
||||
import { Icon, showContextMenu, type MenuItem } from "@/ui";
|
||||
import { Dialog, Icon, showContextMenu, type MenuItem } from "@/ui";
|
||||
import { parseDelimited, toCsv, toTsv, type Cell } from "./model";
|
||||
import styles from "./Database.module.css";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { CELL_PREVIEW_LIMIT, cellPreview } from "./cellPreview";
|
||||
|
||||
export interface GridColumn {
|
||||
name: string;
|
||||
@@ -103,7 +104,7 @@ export function Grid(props: GridProps) {
|
||||
window.addEventListener("mouseup", stop);
|
||||
return () => window.removeEventListener("mouseup", stop);
|
||||
}, []);
|
||||
const [editing, setEditing] = useState<{ row: number; col: number; text: string } | null>(null);
|
||||
const [editing, setEditing] = useState<{ row: number; col: number; text: string; dialog: boolean } | null>(null);
|
||||
const columnKey = columns.map((c) => c.name).join("\u0000");
|
||||
const [widths, setWidths] = useState<number[]>([]);
|
||||
const [numeric, setNumeric] = useState<boolean[]>([]);
|
||||
@@ -171,7 +172,8 @@ export function Grid(props: GridProps) {
|
||||
|
||||
const startEdit = (row: number, col: number) => {
|
||||
if (!editable || props.rowState?.(row) === "deleted") return;
|
||||
setEditing({ row, col, text: value(row, col) ?? "" });
|
||||
const text = value(row, col) ?? "";
|
||||
setEditing({ row, col, text, dialog: text.length > CELL_PREVIEW_LIMIT });
|
||||
};
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
@@ -302,6 +304,29 @@ export function Grid(props: GridProps) {
|
||||
onPaste={onPaste}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{editing?.dialog && (
|
||||
<Dialog
|
||||
open
|
||||
title={tx`Edit ${columns[editing.col]!.name}`}
|
||||
width={720}
|
||||
onClose={() => setEditing(null)}
|
||||
buttons={[
|
||||
{ label: "Cancel", role: "cancel", onClick: () => setEditing(null) },
|
||||
{ label: "Apply", role: "default", onClick: commit },
|
||||
]}
|
||||
>
|
||||
<textarea
|
||||
className={styles.largeCellEditor}
|
||||
aria-label={`Edit ${columns[editing.col]!.name}`}
|
||||
value={editing.text}
|
||||
spellCheck={false}
|
||||
wrap="soft"
|
||||
onContextMenu={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setEditing({ ...editing, text: e.target.value })}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
<div className={styles.gridInner} style={{ width: totalWidth, height: virtualizer.getTotalSize() + ROW_H }}>
|
||||
<div className={clsx(styles.gridRow, styles.gridHeader)} style={{ gridTemplateColumns: template }} role="row">
|
||||
<div className={styles.rowNumber} />
|
||||
@@ -368,7 +393,7 @@ export function Grid(props: GridProps) {
|
||||
{state === "inserted" ? "+" : (props.rowNumber?.(r) ?? firstRowNumber + r)}
|
||||
</div>
|
||||
{columns.map((_, c) => {
|
||||
if (editing && editing.row === r && editing.col === c) {
|
||||
if (editing && !editing.dialog && editing.row === r && editing.col === c) {
|
||||
return (
|
||||
<input
|
||||
key={c}
|
||||
@@ -407,7 +432,11 @@ export function Grid(props: GridProps) {
|
||||
onDoubleClick={() => startEdit(r, c)}
|
||||
title={val != null && val.length > 40 ? val.slice(0, 2000) : undefined}
|
||||
>
|
||||
{val === null ? <span className={styles.null}><null></span> : displayed(val, !!numeric[c])}
|
||||
{val === null ? (
|
||||
<span className={styles.null}><null></span>
|
||||
) : (
|
||||
cellPreview(displayed(val, !!numeric[c]))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const CELL_PREVIEW_LIMIT = 1000;
|
||||
|
||||
/** Bound text layout without changing the underlying value or splitting a surrogate pair. */
|
||||
export function cellPreview(value: string): string {
|
||||
if (value.length <= CELL_PREVIEW_LIMIT) return value;
|
||||
let end = CELL_PREVIEW_LIMIT;
|
||||
const last = value.charCodeAt(end - 1);
|
||||
if (last >= 0xd800 && last <= 0xdbff) end--;
|
||||
return value.slice(0, end) + "\u2026";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Edit {name}": "编辑 {name}",
|
||||
"The language server JDK ({path}) must be a full JDK with jdk.compiler, jdk.jdi and jdk.unsupported for Java language support and debugging. jdtls needs JDK {version} or newer. Download JDK {version}, choose a full JDK in {setting}, or set it to Auto.": "语言服务器 JDK({path})必须是包含 jdk.compiler、jdk.jdi 和 jdk.unsupported 的完整 JDK,才能支持 Java 语言功能和调试。jdtls 需要 JDK {version} 或更高版本。请下载 JDK {version},在 {setting} 中选择完整 JDK,或设为自动。",
|
||||
"Apple's clangd does not run clang-tidy, so checks from .clang-tidy are not shown. Install clangd (LLVM) to get them.": "Apple 的 clangd 不运行 clang-tidy,因此不会显示 .clang-tidy 中配置的检查。请安装 clangd(LLVM)以启用这些检查。",
|
||||
"Newer dependency version available": "有更新的依赖版本可用",
|
||||
|
||||
Reference in new issue
Block a user