дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(vcs): reorder Git log columns by dragging headers (#1856)
The Git Log column order was fixed. Column headers can now be dragged to any position, including the commit column, with an insertion line showing where the column lands. It uses pointer events because WebView2 intercepts HTML5 drag events, and the order persists across restarts. Siblings covered: Esc or losing the window cancels a drag and releases pointer capture, and keyboard column moves skip hidden columns. Co-authored-by: muutot <[email protected]>
Цей коміт міститься в:
1 джерело
fff2c35181
коміт
f04164d7ee
3 files changed
+375
-137
No files matched your search
@@ -207,6 +207,17 @@
|
||||
position: relative;
|
||||
padding-inline-end: 6px;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.columnHeading.dragSource {
|
||||
cursor: grabbing;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.columnHeading.dropBefore {
|
||||
box-shadow: inset 2px 0 0 var(--accent);
|
||||
}
|
||||
.columnHeading.dropAfter {
|
||||
box-shadow: inset -2px 0 0 var(--accent);
|
||||
}
|
||||
.columnResize {
|
||||
position: absolute;
|
||||
|
||||
+257
-136
@@ -1728,17 +1728,7 @@ function CommitList() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
{opts.showColumnHeaders && (
|
||||
<div className={clsx(styles.row, styles.columnHeader)} style={{ minWidth }}>
|
||||
{opts.showRepository && useVcs.getState().roots.length > 1 && (
|
||||
<span className={styles.colRepository}>{tx("Repository")}</span>
|
||||
)}
|
||||
<span className={styles.colSubject}>{tx("Commit")}</span>
|
||||
{columns.map((column) => (
|
||||
<LogColumnHeader key={column.id} column={column} columns={columns} options={opts} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{opts.showColumnHeaders && <ColumnHeaders columns={columns} options={opts} minWidth={minWidth} />}
|
||||
{!commits.length && !loading && finished && (
|
||||
<div className={styles.message}>{headCommit ? tx("No commits match the filters") : tx("No commits yet")}</div>
|
||||
)}
|
||||
@@ -1789,102 +1779,226 @@ function CommitList() {
|
||||
);
|
||||
}
|
||||
|
||||
function LogColumnHeader({
|
||||
column,
|
||||
interface ColumnDrag {
|
||||
id: string;
|
||||
target: string | null;
|
||||
after: boolean;
|
||||
}
|
||||
// The flexible "Commit" column shares the same order and can be dragged like the rest.
|
||||
const SUBJECT_COLUMN: LogColumn = { id: "subject", title: "Commit", width: 0, render: () => "" };
|
||||
// Until the user drags the commit column somewhere, it leads the row.
|
||||
const columnSequence = (columnOrder: string[], builtins: LogColumn[]): string[] => {
|
||||
const ids = [...new Set([...columnOrder, ...builtins.map((c) => c.id)])];
|
||||
return columnOrder.includes(SUBJECT_COLUMN.id) ? ids : [SUBJECT_COLUMN.id, ...ids];
|
||||
};
|
||||
function ColumnHeaders({
|
||||
columns,
|
||||
options,
|
||||
minWidth,
|
||||
}: {
|
||||
column: LogColumn;
|
||||
columns: LogColumn[];
|
||||
options: LogOptions;
|
||||
minWidth: number;
|
||||
}) {
|
||||
const drag = useRef<{ x: number; width: number } | null>(null);
|
||||
const [drag, setDrag] = useState<ColumnDrag | null>(null);
|
||||
const ids = columnSequence(options.columnOrder, columns);
|
||||
const cancel = useCallback(() => setDrag(null), []);
|
||||
const move = (source: string, target: string | null, after: boolean) => {
|
||||
if (!target || source === target || !ids.includes(source) || !ids.includes(target)) return;
|
||||
const reordered = ids.filter((id) => id !== source);
|
||||
const to = reordered.indexOf(target);
|
||||
reordered.splice(after ? to + 1 : to, 0, source);
|
||||
updateLogOptions({ columnOrder: reordered });
|
||||
};
|
||||
const visibleIds = ids.filter((id) => id === SUBJECT_COLUMN.id || columns.some((column) => column.id === id));
|
||||
return (
|
||||
<div className={clsx(styles.row, styles.columnHeader)} style={{ minWidth }}>
|
||||
{options.showRepository && useVcs.getState().roots.length > 1 && (
|
||||
<span className={styles.colRepository}>{tx("Repository")}</span>
|
||||
)}
|
||||
{ids.map((cid) => {
|
||||
const column = cid === SUBJECT_COLUMN.id ? SUBJECT_COLUMN : columns.find((c) => c.id === cid);
|
||||
if (!column) return null;
|
||||
return (
|
||||
<LogColumnHeader
|
||||
key={cid}
|
||||
column={column}
|
||||
subject={cid === SUBJECT_COLUMN.id}
|
||||
ids={visibleIds}
|
||||
options={options}
|
||||
drag={drag}
|
||||
onMove={move}
|
||||
onDragOver={(id, target, after) =>
|
||||
setDrag((d) => (d?.id === id && d.target === target && d.after === after ? d : { id, target, after }))
|
||||
}
|
||||
onDragCancel={cancel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Column headers reorder by pointer capture: WebView2 blocks HTML5 drag events
|
||||
// while dragDropEnabled is on, so dragging must not rely on them.
|
||||
function LogColumnHeader({
|
||||
column,
|
||||
subject,
|
||||
ids,
|
||||
options,
|
||||
drag,
|
||||
onMove,
|
||||
onDragOver,
|
||||
onDragCancel,
|
||||
}: {
|
||||
column: LogColumn;
|
||||
subject?: boolean;
|
||||
ids: string[];
|
||||
options: LogOptions;
|
||||
drag: ColumnDrag | null;
|
||||
onMove(source: string, target: string | null, after: boolean): void;
|
||||
onDragOver(id: string, target: string | null, after: boolean): void;
|
||||
onDragCancel(): void;
|
||||
}) {
|
||||
const press = useRef<{
|
||||
x: number;
|
||||
y: number;
|
||||
active: boolean;
|
||||
element: HTMLElement;
|
||||
pointerId: number;
|
||||
} | null>(null);
|
||||
const resizeDrag = useRef<{ x: number; width: number; element: HTMLElement; pointerId: number } | null>(null);
|
||||
const cancel = useCallback(() => {
|
||||
const captures = [press.current, resizeDrag.current];
|
||||
press.current = null;
|
||||
resizeDrag.current = null;
|
||||
for (const capture of captures)
|
||||
if (capture?.element.hasPointerCapture?.(capture.pointerId))
|
||||
capture.element.releasePointerCapture(capture.pointerId);
|
||||
onDragCancel();
|
||||
}, [onDragCancel]);
|
||||
useEffect(() => {
|
||||
const escape = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key !== "Escape" || (!press.current && !resizeDrag.current)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
cancel();
|
||||
};
|
||||
window.addEventListener("blur", cancel);
|
||||
window.addEventListener("keydown", escape, true);
|
||||
return () => {
|
||||
window.removeEventListener("blur", cancel);
|
||||
window.removeEventListener("keydown", escape, true);
|
||||
cancel();
|
||||
};
|
||||
}, [cancel]);
|
||||
const width = options.columnWidths[column.id] ?? column.width ?? 120;
|
||||
const resize = (value: number) =>
|
||||
updateLogOptions({
|
||||
columnWidths: { ...logOptions().columnWidths, [column.id]: Math.max(40, Math.min(800, value)) },
|
||||
});
|
||||
const move = (source: string, target: string) => {
|
||||
const ids = columns.map((c) => c.id);
|
||||
const from = ids.indexOf(source),
|
||||
to = ids.indexOf(target);
|
||||
if (from < 0 || to < 0 || from === to) return;
|
||||
ids.splice(from, 1);
|
||||
ids.splice(to, 0, source);
|
||||
updateLogOptions({ columnOrder: ids });
|
||||
const index = ids.indexOf(column.id);
|
||||
const hitColumn = (x: number, y: number) => {
|
||||
const el = document.elementFromPoint(x, y)?.closest?.("[data-log-column]");
|
||||
if (!el || el.parentElement !== press.current?.element.parentElement) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { id: el.getAttribute("data-log-column")!, after: x > rect.left + rect.width / 2 };
|
||||
};
|
||||
const index = columns.findIndex((c) => c.id === column.id);
|
||||
return (
|
||||
<span
|
||||
className={clsx(styles.columnCell, styles.columnHeading)}
|
||||
style={{ width }}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData("application/x-openidea-log-column", column.id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
className={clsx(
|
||||
styles.columnCell,
|
||||
styles.columnHeading,
|
||||
subject && styles.colSubject,
|
||||
drag?.id === column.id && styles.dragSource,
|
||||
drag &&
|
||||
drag.target === column.id &&
|
||||
drag.id !== column.id &&
|
||||
(drag.after ? styles.dropAfter : styles.dropBefore),
|
||||
)}
|
||||
style={subject ? undefined : { width }}
|
||||
data-log-column={column.id}
|
||||
onPointerDown={(e) => {
|
||||
if (e.button !== 0) return;
|
||||
press.current = { x: e.clientX, y: e.clientY, active: false, element: e.currentTarget, pointerId: e.pointerId };
|
||||
e.currentTarget.setPointerCapture?.(e.pointerId);
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
if (e.dataTransfer.types.includes("application/x-openidea-log-column")) e.preventDefault();
|
||||
onPointerMove={(e) => {
|
||||
const start = press.current;
|
||||
if (!start || start.pointerId !== e.pointerId) return;
|
||||
if (!start.active && Math.hypot(e.clientX - start.x, e.clientY - start.y) < 4) return;
|
||||
start.active = true;
|
||||
const target = hitColumn(e.clientX, e.clientY);
|
||||
onDragOver(column.id, target?.id ?? null, target?.after ?? false);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
move(e.dataTransfer.getData("application/x-openidea-log-column"), column.id);
|
||||
onPointerUp={(e) => {
|
||||
const start = press.current;
|
||||
if (!start || start.pointerId !== e.pointerId) return;
|
||||
const target = hitColumn(e.clientX, e.clientY);
|
||||
if (start.active) onMove(column.id, target?.id ?? null, target?.after ?? false);
|
||||
cancel();
|
||||
}}
|
||||
onPointerCancel={cancel}
|
||||
onLostPointerCapture={cancel}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
showContextMenu(e, [
|
||||
{
|
||||
label: "Move Left",
|
||||
disabled: index === 0,
|
||||
run: () => move(column.id, columns[index - 1]?.id ?? column.id),
|
||||
run: () => onMove(column.id, ids[index - 1] ?? null, false),
|
||||
},
|
||||
{
|
||||
label: "Move Right",
|
||||
disabled: index === columns.length - 1,
|
||||
run: () => move(column.id, columns[index + 1]?.id ?? column.id),
|
||||
disabled: index === ids.length - 1,
|
||||
run: () => onMove(column.id, ids[index + 1] ?? null, true),
|
||||
},
|
||||
{ label: "Reset Width", run: () => resize(column.width ?? 120) },
|
||||
...(subject ? [] : [{ label: "Reset Width", run: () => resize(column.width ?? 120) }]),
|
||||
]);
|
||||
}}
|
||||
>
|
||||
{tx(column.title)}
|
||||
<span
|
||||
className={styles.columnResize}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={tx`Resize ${tx(column.title)} column`}
|
||||
aria-valuemin={40}
|
||||
aria-valuemax={800}
|
||||
aria-valuenow={width}
|
||||
tabIndex={0}
|
||||
onPointerDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
drag.current = { x: e.clientX, width };
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
}}
|
||||
onPointerMove={(e) => {
|
||||
if (drag.current && e.currentTarget.hasPointerCapture(e.pointerId))
|
||||
resize(drag.current.width + e.clientX - drag.current.x);
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
drag.current = null;
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
drag.current = null;
|
||||
}}
|
||||
onLostPointerCapture={() => {
|
||||
drag.current = null;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.altKey) move(column.id, columns[index + (e.key === "ArrowLeft" ? -1 : 1)]?.id ?? column.id);
|
||||
else resize(width + (e.key === "ArrowLeft" ? -10 : 10));
|
||||
}}
|
||||
/>
|
||||
{!subject && (
|
||||
<span
|
||||
className={styles.columnResize}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={tx`Resize ${tx(column.title)} column`}
|
||||
aria-valuemin={40}
|
||||
aria-valuemax={800}
|
||||
aria-valuenow={width}
|
||||
tabIndex={0}
|
||||
onPointerDown={(e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resizeDrag.current = { x: e.clientX, width, element: e.currentTarget, pointerId: e.pointerId };
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
}}
|
||||
onPointerMove={(e) => {
|
||||
if (resizeDrag.current && e.currentTarget.hasPointerCapture(e.pointerId))
|
||||
resize(resizeDrag.current.width + e.clientX - resizeDrag.current.x);
|
||||
}}
|
||||
onPointerUp={(e) => {
|
||||
resizeDrag.current = null;
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
}}
|
||||
onPointerCancel={() => {
|
||||
resizeDrag.current = null;
|
||||
}}
|
||||
onLostPointerCapture={() => {
|
||||
resizeDrag.current = null;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.altKey)
|
||||
onMove(column.id, ids[index + (e.key === "ArrowLeft" ? -1 : 1)] ?? null, e.key === "ArrowRight");
|
||||
else resize(width + (e.key === "ArrowLeft" ? -10 : 10));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1979,6 +2093,53 @@ function CommitRowView({
|
||||
left={opts.refsOnLeft}
|
||||
/>
|
||||
);
|
||||
const subjectCell = (
|
||||
<span key={SUBJECT_COLUMN.id} className={styles.colSubject}>
|
||||
{graph && (
|
||||
<GraphCell
|
||||
row={graph}
|
||||
head={head}
|
||||
activeColor={hoverColor}
|
||||
highlighted={highlighted}
|
||||
highlightedNodes={highlightedNodes}
|
||||
onHover={onHover}
|
||||
onNavigate={onNavigate}
|
||||
targetTitle={targetTitle}
|
||||
onFragment={(target, expand, source) => onFragment(source ?? commitKey(c), target, expand)}
|
||||
onNode={() => onFragment(commitKey(c), commitKey(c), false)}
|
||||
interactive={interactive}
|
||||
/>
|
||||
)}
|
||||
{bookmarked && <Icon name="bookmark" size={12} />}
|
||||
{opts.refsOnLeft && refs}
|
||||
<span
|
||||
ref={subjectRef}
|
||||
className={clsx(styles.subject, head && styles.headSubject, merge && styles.mergeSubject)}
|
||||
onPointerEnter={enterTooltip}
|
||||
onPointerLeave={leaveTooltip}
|
||||
onPointerDown={() => {
|
||||
clearTimeout(tooltipTimer.current);
|
||||
setTooltip(false);
|
||||
}}
|
||||
>
|
||||
<IssueText text={c.summary} />
|
||||
</span>
|
||||
{!opts.refsOnLeft && refs}
|
||||
<Popup
|
||||
open={tooltip}
|
||||
anchor={subjectRef}
|
||||
role="tooltip"
|
||||
autoFocus={false}
|
||||
restoreFocus={false}
|
||||
onClose={() => setTooltip(false)}
|
||||
className={styles.commitTooltip}
|
||||
>
|
||||
<div onPointerEnter={() => clearTimeout(tooltipTimer.current)} onPointerLeave={leaveTooltip}>
|
||||
<CommitMessage commit={c} />
|
||||
</div>
|
||||
</Popup>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
@@ -2000,66 +2161,26 @@ function CommitRowView({
|
||||
{repoName(commitRepo(c)!)}
|
||||
</span>
|
||||
)}
|
||||
<span className={styles.colSubject}>
|
||||
{graph && (
|
||||
<GraphCell
|
||||
row={graph}
|
||||
head={head}
|
||||
activeColor={hoverColor}
|
||||
highlighted={highlighted}
|
||||
highlightedNodes={highlightedNodes}
|
||||
onHover={onHover}
|
||||
onNavigate={onNavigate}
|
||||
targetTitle={targetTitle}
|
||||
onFragment={(target, expand, source) => onFragment(source ?? commitKey(c), target, expand)}
|
||||
onNode={() => onFragment(commitKey(c), commitKey(c), false)}
|
||||
interactive={interactive}
|
||||
/>
|
||||
)}
|
||||
{bookmarked && <Icon name="bookmark" size={12} />}
|
||||
{opts.refsOnLeft && refs}
|
||||
<span
|
||||
ref={subjectRef}
|
||||
className={clsx(styles.subject, head && styles.headSubject, merge && styles.mergeSubject)}
|
||||
onPointerEnter={enterTooltip}
|
||||
onPointerLeave={leaveTooltip}
|
||||
onPointerDown={() => {
|
||||
clearTimeout(tooltipTimer.current);
|
||||
setTooltip(false);
|
||||
}}
|
||||
>
|
||||
<IssueText text={c.summary} />
|
||||
</span>
|
||||
{!opts.refsOnLeft && refs}
|
||||
<Popup
|
||||
open={tooltip}
|
||||
anchor={subjectRef}
|
||||
role="tooltip"
|
||||
autoFocus={false}
|
||||
restoreFocus={false}
|
||||
onClose={() => setTooltip(false)}
|
||||
className={styles.commitTooltip}
|
||||
>
|
||||
<div onPointerEnter={() => clearTimeout(tooltipTimer.current)} onPointerLeave={leaveTooltip}>
|
||||
<CommitMessage commit={c} />
|
||||
</div>
|
||||
</Popup>
|
||||
</span>
|
||||
{columns.map((column) => (
|
||||
<span
|
||||
key={column.id}
|
||||
className={clsx(
|
||||
styles.columnCell,
|
||||
styles.muted,
|
||||
column.id === "author" && mine && styles.mine,
|
||||
column.id === "hash" && styles.hash,
|
||||
)}
|
||||
style={{ width: opts.columnWidths[column.id] ?? column.width ?? 120 }}
|
||||
title={column.id === "author" ? `${c.author} <${c.email}>` : undefined}
|
||||
>
|
||||
{column.render(c)}
|
||||
</span>
|
||||
))}
|
||||
{columnSequence(opts.columnOrder, columns).map((cid) => {
|
||||
if (cid === SUBJECT_COLUMN.id) return subjectCell;
|
||||
const column = columns.find((col) => col.id === cid);
|
||||
if (!column) return null;
|
||||
return (
|
||||
<span
|
||||
key={cid}
|
||||
className={clsx(
|
||||
styles.columnCell,
|
||||
styles.muted,
|
||||
column.id === "author" && mine && styles.mine,
|
||||
column.id === "hash" && styles.hash,
|
||||
)}
|
||||
style={{ width: opts.columnWidths[column.id] ?? column.width ?? 120 }}
|
||||
title={column.id === "author" ? `${c.author} <${c.email}>` : undefined}
|
||||
>
|
||||
{column.render(c)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,113 @@ describe("log controls", () => {
|
||||
fireEvent.keyDown(author, { key: "ArrowRight" });
|
||||
expect(useLogOptions.getState().columnWidths.author).toBe(140);
|
||||
fireEvent.keyDown(author, { key: "ArrowRight", altKey: true });
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(["date", "author"]);
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(["subject", "date", "author", "hash"]);
|
||||
});
|
||||
|
||||
it.each([{ order: [] }, { order: ["date"] }, { order: ["unknown", "date", "author", "date"] }])(
|
||||
"restores an older column order safely: $order",
|
||||
async ({ order }) => {
|
||||
useLogOptions.setState({ showColumnHeaders: true, columnOrder: order });
|
||||
render(<LogView />);
|
||||
await screen.findByRole("separator", { name: "Resize Author column" });
|
||||
const expected = order.includes("date") ? ["subject", "date", "author"] : ["subject", "author", "date"];
|
||||
expect(
|
||||
[...document.querySelectorAll<HTMLElement>("[data-log-column]")].map((el) => el.dataset.logColumn),
|
||||
).toEqual(expected);
|
||||
const author = screen.getByRole("separator", { name: "Resize Author column" });
|
||||
fireEvent.keyDown(author, { key: "ArrowLeft", altKey: true });
|
||||
expect(useLogOptions.getState().columnOrder).not.toContain(undefined);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps pointer resizing separate from reordering and releases its capture on blur", async () => {
|
||||
useLogOptions.setState({ showColumnHeaders: true });
|
||||
render(<LogView />);
|
||||
const author = await screen.findByRole("separator", { name: "Resize Author column" });
|
||||
const release = vi.fn();
|
||||
author.setPointerCapture = vi.fn();
|
||||
author.hasPointerCapture = () => true;
|
||||
author.releasePointerCapture = release;
|
||||
fireEvent.pointerDown(author, { button: 0, pointerId: 1, clientX: 10 });
|
||||
fireEvent.pointerMove(author, { pointerId: 1, clientX: 40 });
|
||||
expect(useLogOptions.getState().columnWidths.author).toBe(160);
|
||||
fireEvent.blur(window);
|
||||
expect(release).toHaveBeenCalledWith(1);
|
||||
fireEvent.pointerMove(author, { pointerId: 1, clientX: 100 });
|
||||
expect(useLogOptions.getState().columnWidths.author).toBe(160);
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(DEFAULT_LOG_OPTIONS.columnOrder);
|
||||
});
|
||||
|
||||
it.each(["Escape", "outside", "blur", "pointercancel", "lostpointercapture"])(
|
||||
"cancels header dragging on %s",
|
||||
async (reason) => {
|
||||
useLogOptions.setState({ showColumnHeaders: true, columnOrder: ["subject", "author", "date"] });
|
||||
render(<LogView />);
|
||||
await screen.findByRole("separator", { name: "Resize Author column" });
|
||||
const author = document.querySelector<HTMLElement>('[data-log-column="author"]')!;
|
||||
const date = document.querySelector<HTMLElement>('[data-log-column="date"]')!;
|
||||
const hit = vi.fn((): Element | null => date);
|
||||
const original = document.elementFromPoint;
|
||||
document.elementFromPoint = hit;
|
||||
const release = vi.fn();
|
||||
author.setPointerCapture = vi.fn();
|
||||
author.hasPointerCapture = () => true;
|
||||
author.releasePointerCapture = release;
|
||||
try {
|
||||
fireEvent.pointerDown(author, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
|
||||
fireEvent.pointerMove(author, { pointerId: 1, clientX: 100, clientY: 10 });
|
||||
fireEvent.pointerMove(author, { pointerId: 1, clientX: 110, clientY: 10 });
|
||||
if (reason === "Escape") fireEvent.keyDown(window, { key: "Escape" });
|
||||
else if (reason === "blur") fireEvent.blur(window);
|
||||
else if (reason === "outside") hit.mockReturnValue(null);
|
||||
else fireEvent(author, new PointerEvent(reason, { bubbles: true, pointerId: 1 }));
|
||||
fireEvent.pointerUp(author, { pointerId: 1, clientX: 110, clientY: 10 });
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(["subject", "author", "date"]);
|
||||
if (reason !== "lostpointercapture") expect(release).toHaveBeenCalledWith(1);
|
||||
} finally {
|
||||
document.elementFromPoint = original;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("moves the commit cell and graph together, persists order, and keeps hidden columns", async () => {
|
||||
useLogOptions.setState({ showColumnHeaders: true, columnOrder: ["subject", "hash", "author", "unknown", "date"] });
|
||||
render(<LogView />);
|
||||
const list = await screen.findByRole("listbox", { name: "Commits" });
|
||||
const subject = document.querySelector<HTMLElement>('[data-log-column="subject"]')!;
|
||||
const date = document.querySelector<HTMLElement>('[data-log-column="date"]')!;
|
||||
const original = document.elementFromPoint;
|
||||
document.elementFromPoint = () => date;
|
||||
try {
|
||||
fireEvent.pointerDown(subject, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
|
||||
fireEvent.pointerMove(subject, { pointerId: 1, clientX: 12, clientY: 10 });
|
||||
fireEvent.pointerUp(subject, { pointerId: 1, clientX: 12, clientY: 10 });
|
||||
expect(useLogOptions.getState().columnOrder[0]).toBe("subject");
|
||||
fireEvent.pointerDown(subject, { button: 0, pointerId: 1, clientX: 10, clientY: 10 });
|
||||
fireEvent.pointerMove(subject, { pointerId: 1, clientX: 100, clientY: 10 });
|
||||
fireEvent.pointerUp(subject, { pointerId: 1, clientX: 100, clientY: 10 });
|
||||
} finally {
|
||||
document.elementFromPoint = original;
|
||||
}
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(["hash", "author", "unknown", "date", "subject"]);
|
||||
expect(JSON.parse(localStorage.getItem("openidea.vcs.logOptions")!).columnOrder).toEqual(
|
||||
useLogOptions.getState().columnOrder,
|
||||
);
|
||||
const row = within(list).getAllByRole("option")[0]!;
|
||||
expect(row.lastElementChild).toContainElement(within(row).getByLabelText("Commit graph"));
|
||||
expect(row.firstElementChild).toHaveTextContent("Author");
|
||||
act(() => useLogOptions.setState({ showHash: true }));
|
||||
expect([...document.querySelectorAll<HTMLElement>("[data-log-column]")].map((el) => el.dataset.logColumn)).toEqual([
|
||||
"hash",
|
||||
"author",
|
||||
"date",
|
||||
"subject",
|
||||
]);
|
||||
const hash = screen.getByRole("separator", { name: "Resize Hash column" });
|
||||
fireEvent.keyDown(hash, { key: "ArrowLeft", altKey: true });
|
||||
expect(useLogOptions.getState().columnOrder).toEqual(["hash", "author", "unknown", "date", "subject"]);
|
||||
fireEvent.keyDown(list, { key: "b" });
|
||||
expect(useGitLog.getState().selected).toBe("/repo\0base");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Заблокувати користувача