Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 124 additions & 119 deletions docs/qa/user-stories.csv

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src-tauri/src/commands/file_association.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tauri::{AppHandle, Manager};
#[cfg(target_os = "windows")]
const FILE_ASSOCIATION_PROG_ID: &str = "CMTraceOpen.LogFile";
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
const LOG_FILE_EXTENSIONS: &[&str] = &[".log", ".lo_", ".log_"];
const LOG_FILE_EXTENSIONS: &[&str] = &[".log", ".lo_", ".log_", ".cmtlog"];
const FILE_ASSOCIATION_PROMPT_FILE_NAME: &str = "file-association-preferences.json";

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
Expand Down Expand Up @@ -288,7 +288,7 @@ mod tests {

#[test]
fn log_file_extensions_include_each_unique_rotation() {
assert_eq!(LOG_FILE_EXTENSIONS, &[".log", ".lo_", ".log_"]);
assert_eq!(LOG_FILE_EXTENSIONS, &[".log", ".lo_", ".log_", ".cmtlog"]);

let unique_extensions: HashSet<_> = LOG_FILE_EXTENSIONS.iter().copied().collect();
assert_eq!(unique_extensions.len(), LOG_FILE_EXTENSIONS.len());
Expand Down
23 changes: 22 additions & 1 deletion src/components/dialogs/AboutDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { getIdentifier, getName, getTauriVersion, getVersion } from "@tauri-apps/api/app";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { AboutDialog } from "./AboutDialog";
Expand Down Expand Up @@ -31,6 +31,27 @@ describe("AboutDialog", () => {
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(await screen.findByText("CMTrace Open")).toBeVisible();
});
it("traps focus and restores focus to the opener", () => {
const opener = document.createElement("button");
document.body.append(opener);
opener.focus();

const view = render(<AboutDialog isOpen onClose={() => {}} />);
const dialog = screen.getByRole("dialog", { name: "About CMTrace Open" });
const ok = screen.getByRole("button", { name: "OK" });

expect(dialog).toHaveAttribute("tabindex", "-1");
expect(document.activeElement).toBe(ok);

fireEvent.keyDown(window, { key: "Tab" });
expect(document.activeElement).toBe(ok);
fireEvent.keyDown(window, { key: "Tab", shiftKey: true });
expect(document.activeElement).toBe(ok);

view.unmount();
expect(document.activeElement).toBe(opener);
opener.remove();
});

it("shows main channel app metadata", async () => {
render(<AboutDialog isOpen onClose={() => {}} />);
Expand Down
8 changes: 7 additions & 1 deletion src/components/dialogs/AboutDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { getIdentifier, getName, getTauriVersion, getVersion } from "@tauri-apps/api/app";
import { tokens } from "@fluentui/react-components";
import { useModalFocus } from "../../hooks/use-modal-focus";
import { getUpdateChannel, getUpdateChannelLabel } from "../../lib/update-channel";

interface AboutDialogProps {
Expand All @@ -13,9 +14,12 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
const [appVersion, setAppVersion] = useState("0.2.0");
const [tauriVersion, setTauriVersion] = useState("-");
const [identifier, setIdentifier] = useState("com.cmtrace.open");
const surfaceRef = useRef<HTMLDivElement>(null);
const updateChannel = getUpdateChannel(appVersion);
const updateChannelLabel = getUpdateChannelLabel(updateChannel);

useModalFocus(isOpen, surfaceRef);

useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -78,6 +82,8 @@ export function AboutDialog({ isOpen, onClose }: AboutDialogProps) {
}}
>
<div
ref={surfaceRef}
tabIndex={-1}
role="dialog"
aria-modal="true"
aria-label="About CMTrace Open"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
6 changes: 3 additions & 3 deletions src/components/dialogs/FileAssociationPromptDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ export function FileAssociationPromptDialog({
</div>

<div style={{ fontSize: "12px", lineHeight: 1.5, marginBottom: "12px" }}>
This standalone copy of CMTrace Open can associate <strong>.log</strong>{" "}
and <strong>.lo_</strong> files so they open directly in the app, similar
to classic CMTrace.exe.
This standalone copy of CMTrace Open can associate <strong>.log</strong>,{" "}
<strong>.log_</strong>, <strong>.lo_</strong>, and <strong>.cmtlog</strong>{" "}
files so they open directly in the app, similar to classic CMTrace.exe.
</div>

<div
Expand Down
7 changes: 4 additions & 3 deletions src/components/dialogs/FilterDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ export function FilterDialog({
);
}
}, [isOpen, currentClauses]);

useEffect(() => {
if (!isOpen) return;

const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && !isFiltering) onClose();
const handleKey = (event: KeyboardEvent) => {
if (event.key === "Escape" && !isFiltering) {
onClose();
}
};

window.addEventListener("keydown", handleKey);
Expand Down
4 changes: 2 additions & 2 deletions src/components/dialogs/UpdateDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react";
import { tokens } from "@fluentui/react-components";
import type { UpdateInfo } from "../../hooks/use-update-checker";
import { useModalFocus } from "../../hooks/use-modal-focus";
import type { UpdateInfo } from "../../hooks/use-update-checker";
import { getUpdateChannelLabel } from "../../lib/update-channel";

interface UpdateDialogProps {
Expand Down Expand Up @@ -46,7 +46,7 @@ export function UpdateDialog({
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, isDownloading, onClose]);
}, [isDownloading, isOpen, onClose]);

// Trigger check when dialog opens via menu (no existing updateInfo)
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ describe("FileAssociationsTab", () => {
screen.getByText(/File associations are only available on Windows/),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Associate \.log files with CMTrace Open/ }),
screen.queryByRole("button", {
name: /Associate \.log, \.log_, \.lo_, and \.cmtlog files with CMTrace Open/,
}),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: /Re-register associations/ }),
Expand All @@ -35,7 +37,9 @@ describe("FileAssociationsTab", () => {
useUiStore.setState({ currentPlatform: "windows" });
render(<FileAssociationsTab />);
expect(
await screen.findByRole("button", { name: /Associate \.log files with CMTrace Open/ }),
await screen.findByRole("button", {
name: "Associate .log, .log_, .lo_, and .cmtlog files with CMTrace Open",
}),
).toBeInTheDocument();
});
});
Expand All @@ -54,6 +58,7 @@ describe("FileAssociationPromptDialog", () => {
it("offers Associate, Don't Ask Again, and Ask Later", () => {
render(<FileAssociationPromptDialog isOpen onClose={vi.fn()} />);
expect(screen.getByText(/Associate log files with CMTrace Open/)).toBeInTheDocument();
expect(screen.getByText(/\.cmtlog/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Associate" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Don't Ask Again" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Ask Later" })).toBeInTheDocument();
Expand Down
8 changes: 4 additions & 4 deletions src/components/dialogs/settings/FileAssociationsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function FileAssociationsTab() {
return (
<div>
<div style={{ fontSize: "12px", color: tokens.colorNeutralForeground3, lineHeight: 1.5 }}>
File associations are only available on Windows. On macOS and Linux, use your system settings to associate .log files with CMTrace Open.
File associations are only available on Windows. On macOS and Linux, use your system settings to associate .log, .log_, .lo_, and .cmtlog files with CMTrace Open.
Comment thread
adamgell marked this conversation as resolved.
</div>
</div>
);
Expand All @@ -73,7 +73,7 @@ export function FileAssociationsTab() {
return (
<div>
<div style={{ fontSize: "12px", color: tokens.colorNeutralForeground3, marginBottom: "16px", lineHeight: 1.5 }}>
Register CMTrace Open as the default handler for .log files on Windows.
Register CMTrace Open as the default handler for .log, .log_, .lo_, and .cmtlog files on Windows.
</div>

{isAssociated === true ? (
Expand All @@ -86,7 +86,7 @@ export function FileAssociationsTab() {
fontWeight: 600,
}}
>
CMTrace Open is currently registered as the handler for .log files.
CMTrace Open is currently registered as the handler for .log, .log_, .lo_, and .cmtlog files.
</div>
<button
type="button"
Expand Down Expand Up @@ -119,7 +119,7 @@ export function FileAssociationsTab() {
fontWeight: 600,
}}
>
Associate .log files with CMTrace Open
Associate .log, .log_, .lo_, and .cmtlog files with CMTrace Open
</button>
)}

Expand Down
8 changes: 7 additions & 1 deletion src/components/log-view/DnsWorkspaceBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ export function DnsWorkspaceBanner() {
useUiStore.getState().ensureWorkspaceVisible("dns-dhcp", "banner");
}, []);

if (!label || dismissed || activeWorkspace !== "log" || !parser || !DNS_PARSER_KINDS.has(parser)) {
if (
!label ||
dismissed ||
activeWorkspace !== "log" ||
!parser ||
!DNS_PARSER_KINDS.has(parser)
) {
return null;
}

Expand Down
94 changes: 78 additions & 16 deletions src/components/registry-view/KeyTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
FolderOpenRegular,
FolderRegular,
} from "@fluentui/react-icons";
import { useVirtualizer } from "@tanstack/react-virtual";
import {
defaultRangeExtractor,
useVirtualizer,
} from "@tanstack/react-virtual";
import { useRegistryStore } from "../../stores/registry-store";
import { flattenVisibleTree } from "../../lib/registry-utils";

Expand All @@ -27,14 +30,7 @@ export function KeyTree() {

const parentRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: flatRows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 20,
});

// Scroll selected row into view when search navigates
// Scroll selected row into view when search navigates.
const selectedIndex = useMemo(
() =>
selectedKeyPath
Expand All @@ -43,15 +39,56 @@ export function KeyTree() {
[flatRows, selectedKeyPath]
);

const rangeExtractor = useCallback(
(range: Parameters<typeof defaultRangeExtractor>[0]) => {
const indexes = defaultRangeExtractor(range);
if (
selectedIndex < 0 ||
selectedIndex >= flatRows.length ||
indexes.includes(selectedIndex)
) {
return indexes;
}
return [...indexes, selectedIndex].sort((a, b) => a - b);
},
[flatRows.length, selectedIndex]
);

const virtualizer = useVirtualizer({
count: flatRows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => ROW_HEIGHT,
overscan: 20,
rangeExtractor,
});

useEffect(() => {
if (selectedIndex >= 0) {
virtualizer.scrollToIndex(selectedIndex, { align: "auto" });
}
}, [selectedIndex, virtualizer]);
const virtualItems = virtualizer.getVirtualItems();
const selectedItemMounted = virtualItems.some(
(virtualRow) => virtualRow.index === selectedIndex,
);

const handleFocus = useCallback(() => {
if (selectedIndex < 0 && flatRows.length > 0) {
setSelectedKeyPath(flatRows[0].node.fullPath);
}
}, [flatRows, selectedIndex, setSelectedKeyPath]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (selectedIndex < 0) return;
if (selectedIndex < 0) {
if (flatRows.length === 0) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const initialIndex = e.key === "ArrowUp" ? flatRows.length - 1 : 0;
setSelectedKeyPath(flatRows[initialIndex].node.fullPath);
}
return;
}
const row = flatRows[selectedIndex];
if (!row) return;

Expand All @@ -63,16 +100,27 @@ export function KeyTree() {
setSelectedKeyPath(flatRows[selectedIndex - 1].node.fullPath);
} else if (e.key === "ArrowRight") {
e.preventDefault();
if (
row.node.children.length > 0 &&
!expandedPaths.has(row.node.fullPath)
) {
toggleExpanded(row.node.fullPath);
if (row.node.children.length > 0) {
if (!expandedPaths.has(row.node.fullPath)) {
toggleExpanded(row.node.fullPath);
} else {
const child = flatRows[selectedIndex + 1];
if (child && child.depth > row.depth) {
setSelectedKeyPath(child.node.fullPath);
}
}
}
} else if (e.key === "ArrowLeft") {
e.preventDefault();
if (expandedPaths.has(row.node.fullPath)) {
toggleExpanded(row.node.fullPath);
} else if (row.depth > 0) {
for (let index = selectedIndex - 1; index >= 0; index--) {
if (flatRows[index].depth < row.depth) {
setSelectedKeyPath(flatRows[index].node.fullPath);
break;
}
}
}
}
},
Expand All @@ -88,22 +136,31 @@ export function KeyTree() {
return (
<div
ref={parentRef}
role="tree"
aria-label="Registry keys"
aria-activedescendant={
selectedItemMounted && selectedIndex >= 0
? `registry-tree-item-${selectedIndex}`
: undefined
}
Comment thread
adamgell marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
tabIndex={0}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
style={{
height: "100%",
overflow: "auto",
outline: "none",
}}
>

<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
{virtualItems.map((virtualRow) => {
const row = flatRows[virtualRow.index];
const isSelected = row.node.fullPath === selectedKeyPath;
const hasChildren = row.node.children.length > 0;
Expand All @@ -112,6 +169,11 @@ export function KeyTree() {
return (
<div
key={row.node.fullPath}
id={`registry-tree-item-${virtualRow.index}`}
role="treeitem"
aria-level={row.depth + 1}
aria-selected={isSelected}
aria-expanded={hasChildren ? isExpanded : undefined}
style={{
position: "absolute",
top: 0,
Expand Down
Loading
Loading