Skip to content

Commit d4ffee7

Browse files
committed
feat: allow access to fiddle history
1 parent bc1b308 commit d4ffee7

16 files changed

+548
-100
lines changed

src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ export enum GlobalSetting {
236236
knownVersion = 'known-electron-versions',
237237
localVersion = 'local-electron-versions',
238238
packageAuthor = 'packageAuthor',
239+
isShowingGistHistory = 'isShowingGistHistory',
239240
packageManager = 'packageManager',
240241
showObsoleteVersions = 'showObsoleteVersions',
241242
showUndownloadedVersions = 'showUndownloadedVersions',

src/less/components/history.less

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.revision-list {
2+
max-height: 400px;
3+
overflow-y: auto;
4+
}
5+
6+
.revision-list ul {
7+
list-style-type: none;
8+
padding: 0;
9+
margin: 0;
10+
}
11+
12+
.revision-item {
13+
padding: 10px;
14+
margin-bottom: 5px;
15+
border-radius: 3px;
16+
cursor: pointer;
17+
transition: background-color 0.2s ease;
18+
border-left: 3px solid #106ba3;
19+
}
20+
21+
.revision-item:hover {
22+
background-color: rgba(167, 182, 194, 0.3);
23+
}
24+
25+
.revision-content {
26+
display: flex;
27+
flex-direction: column;
28+
}
29+
30+
.revision-icon {
31+
margin-right: 8px;
32+
}
33+
34+
.sha-label {
35+
font-size: 12px;
36+
color: #738694;
37+
margin-left: 10px;
38+
font-family: monospace;
39+
}
40+
41+
.revision-details {
42+
display: flex;
43+
justify-content: space-between;
44+
align-items: center;
45+
margin-top: 5px;
46+
}
47+
48+
.revision-date {
49+
color: #738694;
50+
font-size: 12px;
51+
}
52+
53+
.revision-changes {
54+
display: flex;
55+
gap: 5px;
56+
}
57+
58+
.history-loading {
59+
display: flex;
60+
flex-direction: column;
61+
align-items: center;
62+
padding: 20px;
63+
}

src/less/root.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// Components
1111
@import 'components/commands.less';
1212
@import 'components/output.less';
13+
@import 'components/history.less';
1314
@import 'components/dialogs.less';
1415
@import 'components/mosaic.less';
1516
@import 'components/settings.less';

src/renderer/components/commands.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AddressBar } from './commands-address-bar';
99
import { BisectHandler } from './commands-bisect';
1010
import { Runner } from './commands-runner';
1111
import { VersionChooser } from './commands-version-chooser';
12+
import { HistoryWrapper } from './history-wrapper';
1213
import { AppState } from '../state';
1314

1415
interface CommandsProps {
@@ -75,7 +76,14 @@ export const Commands = observer(
7576
<div className="title">{title}</div>
7677
) : undefined}
7778
<div>
78-
<AddressBar appState={appState} />
79+
<ControlGroup vertical={false}>
80+
<AddressBar appState={appState} />
81+
</ControlGroup>
82+
{appState.isShowingGistHistory && (
83+
<ControlGroup vertical={false}>
84+
<HistoryWrapper appState={appState} />
85+
</ControlGroup>
86+
)}
7987
<GistActionButton appState={appState} />
8088
</div>
8189
</div>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as React from 'react';
2+
3+
import { Button } from '@blueprintjs/core';
4+
import { observer } from 'mobx-react';
5+
6+
import { GistHistoryDialog } from './history';
7+
import { AppState } from '../state';
8+
9+
interface HistoryWrapperProps {
10+
appState: AppState;
11+
buttonOnly?: boolean;
12+
className?: string;
13+
}
14+
15+
/**
16+
* A component that observes the appState and manages the history dialog.
17+
* Can be rendered as just a button or as a button with a dialog.
18+
*/
19+
@observer
20+
export class HistoryWrapper extends React.Component<HistoryWrapperProps> {
21+
private toggleHistory = () => {
22+
const { appState } = this.props;
23+
24+
appState.toggleHistory();
25+
26+
setTimeout(() => {
27+
this.forceUpdate();
28+
}, 0);
29+
};
30+
31+
private handleRevisionSelect = async (revisionId: string) => {
32+
const { remoteLoader } = window.app;
33+
try {
34+
await remoteLoader.fetchGistAndLoad(
35+
this.props.appState.gistId!,
36+
revisionId,
37+
);
38+
} catch (error: any) {
39+
console.error('Failed to load revision', error);
40+
this.props.appState.showErrorDialog(
41+
`Failed to load revision: ${error.message || 'Unknown error'}`,
42+
);
43+
}
44+
};
45+
46+
public renderHistoryButton() {
47+
const { className } = this.props;
48+
49+
return (
50+
<Button
51+
icon="history"
52+
onClick={this.toggleHistory}
53+
className={className}
54+
aria-label="View revision history"
55+
data-testid="history-button"
56+
/>
57+
);
58+
}
59+
60+
public render() {
61+
const { appState, buttonOnly } = this.props;
62+
63+
const dialogKey = `history-dialog-${appState.isHistoryShowing}`;
64+
65+
return (
66+
<>
67+
{buttonOnly ? (
68+
this.renderHistoryButton()
69+
) : (
70+
<>
71+
{this.renderHistoryButton()}
72+
<GistHistoryDialog
73+
key={dialogKey}
74+
appState={appState}
75+
isOpen={appState.isHistoryShowing}
76+
onClose={this.toggleHistory}
77+
onRevisionSelect={this.handleRevisionSelect}
78+
/>
79+
</>
80+
)}
81+
</>
82+
);
83+
}
84+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import * as React from 'react';
2+
3+
import {
4+
Classes,
5+
Dialog,
6+
Icon,
7+
NonIdealState,
8+
Spinner,
9+
Tag,
10+
} from '@blueprintjs/core';
11+
import { observer } from 'mobx-react';
12+
13+
import { AppState } from '../state';
14+
15+
interface GistRevision {
16+
sha: string;
17+
date: string;
18+
changes: {
19+
deletions: number;
20+
additions: number;
21+
total: number;
22+
};
23+
}
24+
25+
interface HistoryProps {
26+
appState: AppState;
27+
isOpen: boolean;
28+
onClose: () => void;
29+
onRevisionSelect: (revisionId: string) => Promise<void>;
30+
}
31+
32+
interface HistoryState {
33+
isLoading: boolean;
34+
revisions: GistRevision[];
35+
error: string | null;
36+
}
37+
38+
@observer
39+
export class GistHistoryDialog extends React.Component<
40+
HistoryProps,
41+
HistoryState
42+
> {
43+
constructor(props: HistoryProps) {
44+
super(props);
45+
this.state = {
46+
isLoading: true,
47+
revisions: [],
48+
error: null,
49+
};
50+
}
51+
52+
public async componentDidMount() {
53+
await this.loadRevisions();
54+
}
55+
56+
private async loadRevisions() {
57+
const { appState } = this.props;
58+
const { remoteLoader } = window.app;
59+
60+
if (!appState.gistId) {
61+
this.setState({ isLoading: false, error: 'No Gist ID available' });
62+
return;
63+
}
64+
65+
this.setState({ isLoading: true, error: null });
66+
67+
try {
68+
const revisions = await remoteLoader.getGistRevisions(appState.gistId);
69+
this.setState({ revisions, isLoading: false });
70+
} catch (error) {
71+
console.error('Failed to load gist revisions', error);
72+
this.setState({
73+
isLoading: false,
74+
error: 'Failed to load revision history',
75+
});
76+
}
77+
}
78+
79+
private handleRevisionSelect = async (revision: GistRevision) => {
80+
try {
81+
await this.props.onRevisionSelect(revision.sha);
82+
this.props.onClose();
83+
} catch (error: any) {
84+
console.error('Failed to load revision', error);
85+
// show an error to the user and hide popover
86+
87+
this.props.appState.showErrorDialog(
88+
`Failed to load revision: ${error.message || 'Unknown error'}`,
89+
);
90+
this.props.onClose();
91+
}
92+
};
93+
94+
private renderChangeStats(changes: GistRevision['changes']) {
95+
return (
96+
<div className="revision-changes">
97+
<Tag intent="success" minimal>
98+
+{changes.additions}
99+
</Tag>
100+
<Tag intent="danger" minimal>
101+
-{changes.deletions}
102+
</Tag>
103+
<Tag minimal>{changes.total} total</Tag>
104+
</div>
105+
);
106+
}
107+
108+
private renderRevisionItem = (revision: GistRevision, index: number) => {
109+
const date = new Date(revision.date).toLocaleString();
110+
const shortSha = revision.sha.substring(0, 7);
111+
const titleLabel = index === 0 ? 'Created' : `Revision ${index}`;
112+
113+
return (
114+
<li
115+
key={revision.sha}
116+
className="revision-item"
117+
onClick={() => this.handleRevisionSelect(revision)}
118+
>
119+
<div className="revision-content">
120+
<h4>
121+
<Icon icon="history" className="revision-icon" />
122+
{titleLabel}
123+
<span className="sha-label">{shortSha}</span>
124+
</h4>
125+
<div className="revision-details">
126+
<span className="revision-date">{date}</span>
127+
{this.renderChangeStats(revision.changes)}
128+
</div>
129+
</div>
130+
</li>
131+
);
132+
};
133+
134+
private renderContent() {
135+
const { isLoading, revisions, error } = this.state;
136+
137+
if (isLoading) {
138+
return (
139+
<div className="history-loading">
140+
<Spinner />
141+
<p>Loading revision history...</p>
142+
</div>
143+
);
144+
}
145+
146+
if (error) {
147+
return (
148+
<NonIdealState
149+
icon="error"
150+
title="Error Loading History"
151+
description={error}
152+
/>
153+
);
154+
}
155+
156+
if (revisions.length === 0) {
157+
return (
158+
<NonIdealState
159+
icon="history"
160+
title="No Revision History"
161+
description="This Gist doesn't have any revisions"
162+
/>
163+
);
164+
}
165+
166+
return (
167+
<div className="revision-list">
168+
<ul>{revisions.map(this.renderRevisionItem)}</ul>
169+
</div>
170+
);
171+
}
172+
173+
public render() {
174+
const { isOpen, onClose } = this.props;
175+
176+
return (
177+
<Dialog
178+
isOpen={isOpen}
179+
onClose={onClose}
180+
title="Revision History"
181+
className="gist-history-dialog"
182+
icon="history"
183+
>
184+
<div className={Classes.DIALOG_BODY}>{this.renderContent()}</div>
185+
</Dialog>
186+
);
187+
}
188+
}

0 commit comments

Comments
 (0)