Skip to content

Better Export Filenames#2388

Open
klamping wants to merge 3 commits intoSpecterOps:mainfrom
Mr-Holland-s-Opus:feat/better-export-filenames
Open

Better Export Filenames#2388
klamping wants to merge 3 commits intoSpecterOps:mainfrom
Mr-Holland-s-Opus:feat/better-export-filenames

Conversation

@klamping
Copy link

@klamping klamping commented Feb 19, 2026

Description

Updates the exported filenames to use a generated, human friendly timestamp so that multiple file downloads are more cleanly organized/named.

Motivation and Context

Resolves #1879

How Has This Been Tested?

Via unit tests and manually on my local server instance

Screenshots (optional):

image

Types of changes

It simply updates the filename of the downloaded JSON, however, I think it qualifies as breaking, as if someone is basing some sort of script on the downloaded filename, it will break that script.

  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have met the contributing prerequisites
  • I have ensured that related documentation is up-to-date
    • Open API docs
    • Code comments (GoDocs / JSDocs)
  • I have followed proper test practices
    • Added/updated tests to cover my changes
    • All new and existing tests passed

Summary by CodeRabbit

  • New Features

    • Graph data exports now include a timestamp in the filename instead of using a static name.
  • Tests

    • Added tests for graph data export functionality, including filename generation and download behavior.

@github-actions
Copy link

github-actions bot commented Feb 19, 2026

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@klamping
Copy link
Author

I have read the CLA Document and I hereby sign the CLA

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

Adds timestamp-based filename generation to graph exports, replacing the static 'bh-graph.json' filename with dynamic names like 'bh-graph-YYYY-MM-DD_HH-MM-SS.json'. Includes a new helper function and comprehensive test coverage for the export utilities.

Changes

Cohort / File(s) Summary
Graph Export Utility Implementation
packages/javascript/bh-shared-ui/src/utils/exportGraphData.ts
Added getDefaultGraphExportFileName() helper that generates timestamp-based filenames in ISO format with safe characters. Modified exportToJson() to use this function instead of hardcoded 'bh-graph.json' filename.
Export Utility Tests
packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts
New test file covering filename generation with fixed timestamps and export-to-JSON flow, including DOM API mocking (URL.createObjectURL, document.createElement) and event simulation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Better Export Filenames' accurately summarizes the main change: improving the export filename generation with timestamps instead of static 'bh-graph.json'.
Description check ✅ Passed The description covers all key sections: what changed (timestamped filenames), motivation (resolves #1879), testing approach (unit tests and manual), and identifies it as a breaking change.
Linked Issues check ✅ Passed The PR implementation meets the core requirement from #1879: generating unique default filenames with timestamps (bh-graph-YYYY-MM-DD_HH-MM-SS.json) instead of static 'bh-graph.json'.
Out of Scope Changes check ✅ Passed All changes are directly scoped to improving export filenames: new getDefaultGraphExportFileName() function, updated exportToJson() to use timestamped names, and comprehensive test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts (3)

5-15: Mock/timer cleanup is not failure-safe — move to afterEach

Both tests place cleanup (vi.useRealTimers(), createElementSpy.mockRestore(), global restorations) at the end of the test body. If any expect assertion throws, those cleanup calls are skipped and fake timers / mocked globals leak into subsequent tests, which can cause flaky failures that are hard to diagnose.

Prefer an afterEach hook (or pair with beforeEach) for all teardown:

♻️ Suggested structural refactor
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { describe, expect, it, vi } from 'vitest';
 import * as exportUtils from './exportGraphData';

 describe('exportGraphData', () => {
+    beforeEach(() => {
+        vi.useFakeTimers();
+        vi.setSystemTime(new Date('2026-02-14T20:27:20.000Z'));
+    });
+
+    afterEach(() => {
+        vi.useRealTimers();
+        vi.restoreAllMocks();
+        vi.unstubAllGlobals();
+    });
+
     it('generates a human-friendly default filename for graph exports', () => {
-        vi.useFakeTimers();
-        vi.setSystemTime(new Date('2026-02-14T20:27:20.000Z'));
-
         const name = exportUtils.getDefaultGraphExportFileName();
         expect(name).toBe('bh-graph-2026-02-14_20-27-20.json');
-
-        vi.useRealTimers();
     });

     it('uses the default filename generator when exporting JSON', () => {
-        vi.useFakeTimers();
-        vi.setSystemTime(new Date('2026-02-14T20:27:20.000Z'));
         // ... rest of setup and assertions only
     });
 });

Also applies to: 17-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts` around
lines 5 - 15, Current tests call vi.useRealTimers() and restore spies at the end
of each test body which can be skipped on failure; refactor by adding an
afterEach hook that calls vi.useRealTimers() and restores any mocks/spies (e.g.
createElementSpy.mockRestore()) so cleanup always runs, and keep
vi.useFakeTimers()/vi.setSystemTime(...) in the test or pair with beforeEach if
you prefer; apply the same afterEach teardown for other tests in the file (lines
~17-62) that currently perform manual cleanup.

49-50: Add a guard assertion before accessing createdAnchor

If the createElement spy fails to capture the anchor (e.g., spy not set up correctly), createdAnchor?.download silently evaluates to undefined, and the failure message will be a generic type mismatch rather than "anchor was never captured."

♻️ Proposed improvement
+        expect(createdAnchor).toBeDefined();
         expect(createdAnchor?.download).toBe('bh-graph-2026-02-14_20-27-20.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts` around
lines 49 - 50, Add a guard assertion to ensure the anchor was actually captured
before accessing its properties: check that the test variable createdAnchor (the
element captured from the createElement spy) is defined/non-null (or an instance
of HTMLAnchorElement) with an explicit expect(createdAnchor).toBeDefined() /
toBeTruthy() (or expect(createdAnchor).not.toBeNull()) immediately before the
existing expect(createdAnchor?.download)... assertion so failures clearly
indicate the anchor was never captured rather than producing a generic type
mismatch.

21-60: Prefer vi.stubGlobal / vi.spyOn over manual Object.defineProperty for global mocking

Object.defineProperty without configurable: true may create a non-reconfigurable property if window.URL.createObjectURL (or globalThis.MouseEvent) does not already exist in the test environment. The subsequent restore call (lines 53–55, 57–60) would then throw a TypeError: Cannot redefine property. Vitest's vi.stubGlobal stores the original value automatically and restores it via vi.unstubAllGlobals(). For URL.createObjectURL specifically, vi.stubGlobal("URL", { createObjectURL: () => "..." }) is a documented pattern.

♻️ Suggested idiomatic Vitest mocking
-        const originalCreateObjectURL = (window.URL as any).createObjectURL;
-        const createObjectUrlSpy = vi.fn(() => 'blob:mock');
-        Object.defineProperty(window.URL, 'createObjectURL', {
-            value: createObjectUrlSpy,
-            writable: true,
-        });
+        const createObjectUrlSpy = vi.fn(() => 'blob:mock');
+        vi.stubGlobal('URL', { ...window.URL, createObjectURL: createObjectUrlSpy });

-        const originalMouseEvent = globalThis.MouseEvent;
-        class MockMouseEvent extends Event {
+        class MockMouseEvent extends Event {
             constructor(type: string, _init?: any) {
                 super(type);
             }
         }
-        Object.defineProperty(globalThis, 'MouseEvent', {
-            value: MockMouseEvent,
-            writable: true,
-        });
+        vi.stubGlobal('MouseEvent', MockMouseEvent);

         exportUtils.exportToJson({ a: 1 });

         expect(createdAnchor?.download).toBe('bh-graph-2026-02-14_20-27-20.json');
         expect(createObjectUrlSpy).toHaveBeenCalled();

-        createElementSpy.mockRestore();
-        Object.defineProperty(window.URL, 'createObjectURL', {
-            value: originalCreateObjectURL,
-            writable: true,
-        });
-        Object.defineProperty(globalThis, 'MouseEvent', {
-            value: originalMouseEvent,
-            writable: true,
-        });
-        vi.useRealTimers();
         // cleanup handled by afterEach: vi.restoreAllMocks(), vi.unstubAllGlobals(), vi.useRealTimers()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts` around
lines 21 - 60, Replace manual Object.defineProperty mocks for
window.URL.createObjectURL and globalThis.MouseEvent with Vitest helpers: use
vi.stubGlobal('URL', { createObjectURL: createObjectUrlSpy }) or
vi.stubGlobal('URL', { createObjectURL: () => 'blob:mock' }) to stub
createObjectURL and use vi.stubGlobal('MouseEvent', MockMouseEvent) to stub
MouseEvent; keep the document.createElement spy (createElementSpy) but remove
the manual restoration of URL/MouseEvent and rely on vi.unstubAllGlobals() (or
vi.restoreAllMocks()/vi.resetAllMocks() as appropriate) to restore originals
after calling exportUtils.exportToJson({ a: 1 }) and the expectations. Ensure
createObjectUrlSpy and MockMouseEvent names remain so tests still assert
createObjectUrlSpy calls and createdAnchor behavior.
packages/javascript/bh-shared-ui/src/utils/exportGraphData.ts (1)

32-38: UTC timestamp in filename is correct; optional refactor for local time if preferred

The public API surface is properly configured — getDefaultGraphExportFileName is correctly re-exported through utils/index.ts and the main package index.ts, so downstream consumers can import it directly.

Regarding the timestamp: toISOString() always outputs UTC regardless of user timezone, so a 3:30 PM EST export will show _20-30-00 in the filename. This is unambiguous and fine for uniqueness, but may surprise users expecting local time. If local-time stamps are preferred, consider using new Date().toLocaleString(...) with explicit format tokens or a date library.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.ts` around lines
32 - 38, The current getDefaultGraphExportFileName uses new Date().toISOString()
which yields a UTC timestamp; if you want local-time filenames instead, modify
getDefaultGraphExportFileName to format the current date/time in the local
timezone (e.g., produce YYYY-MM-DD_HH-mm-ss without milliseconds) rather than
using toISOString(); implement this by using Date methods or
Intl.DateTimeFormat/toLocaleString with explicit options (or a date lib) to
build the local date/time parts, replace the 'T' and ':' characters as done now,
and return the same `bh-graph-${safe}.json` pattern so callers of
getDefaultGraphExportFileName keep the same API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.test.ts`:
- Around line 5-15: Current tests call vi.useRealTimers() and restore spies at
the end of each test body which can be skipped on failure; refactor by adding an
afterEach hook that calls vi.useRealTimers() and restores any mocks/spies (e.g.
createElementSpy.mockRestore()) so cleanup always runs, and keep
vi.useFakeTimers()/vi.setSystemTime(...) in the test or pair with beforeEach if
you prefer; apply the same afterEach teardown for other tests in the file (lines
~17-62) that currently perform manual cleanup.
- Around line 49-50: Add a guard assertion to ensure the anchor was actually
captured before accessing its properties: check that the test variable
createdAnchor (the element captured from the createElement spy) is
defined/non-null (or an instance of HTMLAnchorElement) with an explicit
expect(createdAnchor).toBeDefined() / toBeTruthy() (or
expect(createdAnchor).not.toBeNull()) immediately before the existing
expect(createdAnchor?.download)... assertion so failures clearly indicate the
anchor was never captured rather than producing a generic type mismatch.
- Around line 21-60: Replace manual Object.defineProperty mocks for
window.URL.createObjectURL and globalThis.MouseEvent with Vitest helpers: use
vi.stubGlobal('URL', { createObjectURL: createObjectUrlSpy }) or
vi.stubGlobal('URL', { createObjectURL: () => 'blob:mock' }) to stub
createObjectURL and use vi.stubGlobal('MouseEvent', MockMouseEvent) to stub
MouseEvent; keep the document.createElement spy (createElementSpy) but remove
the manual restoration of URL/MouseEvent and rely on vi.unstubAllGlobals() (or
vi.restoreAllMocks()/vi.resetAllMocks() as appropriate) to restore originals
after calling exportUtils.exportToJson({ a: 1 }) and the expectations. Ensure
createObjectUrlSpy and MockMouseEvent names remain so tests still assert
createObjectUrlSpy calls and createdAnchor behavior.

In `@packages/javascript/bh-shared-ui/src/utils/exportGraphData.ts`:
- Around line 32-38: The current getDefaultGraphExportFileName uses new
Date().toISOString() which yields a UTC timestamp; if you want local-time
filenames instead, modify getDefaultGraphExportFileName to format the current
date/time in the local timezone (e.g., produce YYYY-MM-DD_HH-mm-ss without
milliseconds) rather than using toISOString(); implement this by using Date
methods or Intl.DateTimeFormat/toLocaleString with explicit options (or a date
lib) to build the local date/time parts, replace the 'T' and ':' characters as
done now, and return the same `bh-graph-${safe}.json` pattern so callers of
getDefaultGraphExportFileName keep the same API.

a.remove();
};

export const getDefaultGraphExportFileName = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution!

We use luxon internally to handle datetime related logic since that tends to be cumbersome to do without a package. I would recommend reaching for that here so that the formatting is more ergonomic.

Suggested change
export const getDefaultGraphExportFileName = () => {
export const getDefaultGraphExportFileName = () => {
const formattedDate = DateTime.now().toFormat(LuxonFormat.DATETIME_WITHOUT_TIMEZONE)
return `bh-graph-${formattedDate}.json`;
};

There are some other formats to choose from if you have a preference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will get this updated when I get a chance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Better export filenames

2 participants