blob: ca839dfc5ad6f20beda190d36c9b24d0cf7983c8 [file] [log] [blame]
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001// sketch-diff-range-picker.ts
2// Component for selecting commit range for diffs
3
4import { css, html, LitElement } from "lit";
5import { customElement, property, state } from "lit/decorators.js";
6import { GitDataService, DefaultGitDataService } from "./git-data-service";
7import { GitLogEntry } from "../types";
8
9/**
10 * Range type for diff views
11 */
Autoformatter8c463622025-05-16 21:54:17 +000012export type DiffRange =
13 | { type: "range"; from: string; to: string }
14 | { type: "single"; commit: string };
Philip Zeyliger272a90e2025-05-16 14:49:51 -070015
16/**
17 * Component for selecting commit range for diffs
18 */
19@customElement("sketch-diff-range-picker")
20export class SketchDiffRangePicker extends LitElement {
21 @property({ type: Array })
22 commits: GitLogEntry[] = [];
23
24 @state()
Autoformatter8c463622025-05-16 21:54:17 +000025 private rangeType: "range" | "single" = "range";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070026
27 @state()
Autoformatter8c463622025-05-16 21:54:17 +000028 private fromCommit: string = "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070029
30 @state()
Autoformatter8c463622025-05-16 21:54:17 +000031 private toCommit: string = "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070032
33 @state()
Autoformatter8c463622025-05-16 21:54:17 +000034 private singleCommit: string = "";
Philip Zeyliger272a90e2025-05-16 14:49:51 -070035
36 @state()
37 private loading: boolean = true;
38
39 @state()
40 private error: string | null = null;
Autoformatter8c463622025-05-16 21:54:17 +000041
Philip Zeyliger272a90e2025-05-16 14:49:51 -070042 @property({ attribute: false, type: Object })
43 gitService!: GitDataService;
Autoformatter8c463622025-05-16 21:54:17 +000044
Philip Zeyliger272a90e2025-05-16 14:49:51 -070045 constructor() {
46 super();
Autoformatter8c463622025-05-16 21:54:17 +000047 console.log("SketchDiffRangePicker initialized");
Philip Zeyliger272a90e2025-05-16 14:49:51 -070048 }
49
50 static styles = css`
51 :host {
52 display: block;
53 width: 100%;
54 font-family: var(--font-family, system-ui, sans-serif);
55 color: var(--text-color, #333);
56 }
57
58 .range-picker {
59 display: flex;
60 flex-direction: row;
61 align-items: center;
62 gap: 12px;
63 padding: 12px;
64 background-color: var(--background-light, #f8f8f8);
65 border-radius: 4px;
66 border: 1px solid var(--border-color, #e0e0e0);
67 flex-wrap: wrap; /* Allow wrapping on small screens */
68 width: 100%;
69 box-sizing: border-box;
70 }
71
72 .range-type-selector {
73 display: flex;
74 gap: 16px;
75 flex-shrink: 0;
76 }
77
78 .range-type-option {
79 display: flex;
80 align-items: center;
81 gap: 6px;
82 cursor: pointer;
83 white-space: nowrap;
84 }
85
86 .commit-selectors {
87 display: flex;
88 flex-direction: row;
89 align-items: center;
90 gap: 12px;
91 flex: 1;
92 flex-wrap: wrap; /* Allow wrapping on small screens */
93 }
94
95 .commit-selector {
96 display: flex;
97 align-items: center;
98 gap: 8px;
99 flex: 1;
100 min-width: 200px;
101 max-width: calc(50% - 12px); /* Half width minus half the gap */
102 overflow: hidden;
103 }
104
105 select {
106 padding: 6px 8px;
107 border-radius: 4px;
108 border: 1px solid var(--border-color, #e0e0e0);
109 background-color: var(--background, #fff);
110 max-width: 100%;
111 overflow: hidden;
112 text-overflow: ellipsis;
113 white-space: nowrap;
114 }
115
116 label {
117 font-weight: 500;
118 font-size: 14px;
119 }
120
121 .loading {
122 font-style: italic;
123 color: var(--text-muted, #666);
124 }
125
126 .error {
127 color: var(--error-color, #dc3545);
128 font-size: 14px;
129 }
Autoformatter8c463622025-05-16 21:54:17 +0000130
Philip Zeyligere89b3082025-05-29 03:16:06 +0000131 .refresh-button {
132 padding: 6px 12px;
133 background-color: #f0f0f0;
134 color: var(--text-color, #333);
135 border: 1px solid var(--border-color, #e0e0e0);
136 border-radius: 4px;
137 cursor: pointer;
138 font-size: 14px;
139 transition: background-color 0.2s;
140 white-space: nowrap;
141 display: flex;
142 align-items: center;
143 gap: 4px;
144 }
145
146 .refresh-button:hover {
147 background-color: #e0e0e0;
148 }
149
150 .refresh-button:disabled {
151 background-color: #f8f8f8;
152 color: #999;
153 cursor: not-allowed;
154 }
155
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700156 @media (max-width: 768px) {
157 .commit-selector {
158 max-width: 100%;
159 }
160 }
161 `;
162
163 connectedCallback() {
164 super.connectedCallback();
165 // Wait for DOM to be fully loaded to ensure proper initialization order
Autoformatter8c463622025-05-16 21:54:17 +0000166 if (document.readyState === "complete") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700167 this.loadCommits();
168 } else {
Autoformatter8c463622025-05-16 21:54:17 +0000169 window.addEventListener("load", () => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700170 setTimeout(() => this.loadCommits(), 0); // Give time for provider initialization
171 });
172 }
173 }
174
175 render() {
176 return html`
177 <div class="range-picker">
178 ${this.loading
179 ? html`<div class="loading">Loading commits...</div>`
180 : this.error
Autoformatter8c463622025-05-16 21:54:17 +0000181 ? html`<div class="error">${this.error}</div>`
182 : this.renderRangePicker()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700183 </div>
184 `;
185 }
186
187 renderRangePicker() {
188 return html`
189 <div class="range-type-selector">
190 <label class="range-type-option">
191 <input
192 type="radio"
193 name="rangeType"
194 value="range"
Autoformatter8c463622025-05-16 21:54:17 +0000195 ?checked=${this.rangeType === "range"}
196 @change=${() => this.setRangeType("range")}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700197 />
198 Commit Range
199 </label>
200 <label class="range-type-option">
201 <input
202 type="radio"
203 name="rangeType"
204 value="single"
Autoformatter8c463622025-05-16 21:54:17 +0000205 ?checked=${this.rangeType === "single"}
206 @change=${() => this.setRangeType("single")}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700207 />
208 Single Commit
209 </label>
210 </div>
211
212 <div class="commit-selectors">
Autoformatter8c463622025-05-16 21:54:17 +0000213 ${this.rangeType === "range"
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700214 ? this.renderRangeSelectors()
Autoformatter8c463622025-05-16 21:54:17 +0000215 : this.renderSingleSelector()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700216 </div>
Philip Zeyligere89b3082025-05-29 03:16:06 +0000217
218 <button
219 class="refresh-button"
220 @click="${this.handleRefresh}"
221 ?disabled="${this.loading}"
222 title="Refresh commit list"
223 >
224 🔄 Refresh
225 </button>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700226 `;
227 }
228
229 renderRangeSelectors() {
230 return html`
231 <div class="commit-selector">
232 <label for="fromCommit">From:</label>
233 <select
234 id="fromCommit"
235 .value=${this.fromCommit}
236 @change=${this.handleFromChange}
237 >
238 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000239 (commit) => html`
240 <option
241 value=${commit.hash}
242 ?selected=${commit.hash === this.fromCommit}
243 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700244 ${this.formatCommitOption(commit)}
245 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000246 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700247 )}
248 </select>
249 </div>
250 <div class="commit-selector">
251 <label for="toCommit">To:</label>
252 <select
253 id="toCommit"
254 .value=${this.toCommit}
255 @change=${this.handleToChange}
256 >
Autoformatter8c463622025-05-16 21:54:17 +0000257 <option value="" ?selected=${this.toCommit === ""}>
258 Uncommitted Changes
259 </option>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700260 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000261 (commit) => html`
262 <option
263 value=${commit.hash}
264 ?selected=${commit.hash === this.toCommit}
265 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700266 ${this.formatCommitOption(commit)}
267 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000268 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700269 )}
270 </select>
271 </div>
272 `;
273 }
274
275 renderSingleSelector() {
276 return html`
277 <div class="commit-selector">
278 <label for="singleCommit">Commit:</label>
279 <select
280 id="singleCommit"
281 .value=${this.singleCommit}
282 @change=${this.handleSingleChange}
283 >
284 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000285 (commit) => html`
286 <option
287 value=${commit.hash}
288 ?selected=${commit.hash === this.singleCommit}
289 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700290 ${this.formatCommitOption(commit)}
291 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000292 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700293 )}
294 </select>
295 </div>
296 `;
297 }
298
299 /**
300 * Format a commit for display in the dropdown
301 */
302 formatCommitOption(commit: GitLogEntry): string {
303 const shortHash = commit.hash.substring(0, 7);
304 let label = `${shortHash} ${commit.subject}`;
Autoformatter8c463622025-05-16 21:54:17 +0000305
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700306 if (commit.refs && commit.refs.length > 0) {
Autoformatter8c463622025-05-16 21:54:17 +0000307 label += ` (${commit.refs.join(", ")})`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700308 }
Autoformatter8c463622025-05-16 21:54:17 +0000309
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700310 return label;
311 }
312
313 /**
314 * Load commits from the Git data service
315 */
316 async loadCommits() {
317 this.loading = true;
318 this.error = null;
319
320 if (!this.gitService) {
Autoformatter8c463622025-05-16 21:54:17 +0000321 console.error("GitService was not provided to sketch-diff-range-picker");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700322 throw Error();
323 }
324
325 try {
326 // Get the base commit reference
327 const baseCommitRef = await this.gitService.getBaseCommitRef();
Autoformatter8c463622025-05-16 21:54:17 +0000328
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700329 // Load commit history
330 this.commits = await this.gitService.getCommitHistory(baseCommitRef);
Autoformatter8c463622025-05-16 21:54:17 +0000331
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700332 // Set default selections
333 if (this.commits.length > 0) {
334 // For range, default is base to HEAD
335 // TODO: is sketch-base right in the unsafe context, where it's sketch-base-...
336 // should this be startswith?
Autoformatter8c463622025-05-16 21:54:17 +0000337 const baseCommit = this.commits.find(
338 (c) => c.refs && c.refs.some((ref) => ref.includes("sketch-base")),
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700339 );
Autoformatter8c463622025-05-16 21:54:17 +0000340
341 this.fromCommit = baseCommit
342 ? baseCommit.hash
343 : this.commits[this.commits.length - 1].hash;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700344 // Default to Uncommitted Changes by setting toCommit to empty string
Autoformatter8c463622025-05-16 21:54:17 +0000345 this.toCommit = ""; // Empty string represents uncommitted changes
346
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700347 // For single, default to HEAD
348 this.singleCommit = this.commits[0].hash;
Autoformatter8c463622025-05-16 21:54:17 +0000349
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700350 // Dispatch initial range event
351 this.dispatchRangeEvent();
352 }
353 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000354 console.error("Error loading commits:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700355 this.error = `Error loading commits: ${error.message}`;
356 } finally {
357 this.loading = false;
358 }
359 }
360
361 /**
362 * Handle range type change
363 */
Autoformatter8c463622025-05-16 21:54:17 +0000364 setRangeType(type: "range" | "single") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700365 this.rangeType = type;
366 this.dispatchRangeEvent();
367 }
368
369 /**
370 * Handle From commit change
371 */
372 handleFromChange(event: Event) {
373 const select = event.target as HTMLSelectElement;
374 this.fromCommit = select.value;
375 this.dispatchRangeEvent();
376 }
377
378 /**
379 * Handle To commit change
380 */
381 handleToChange(event: Event) {
382 const select = event.target as HTMLSelectElement;
383 this.toCommit = select.value;
384 this.dispatchRangeEvent();
385 }
386
387 /**
388 * Handle Single commit change
389 */
390 handleSingleChange(event: Event) {
391 const select = event.target as HTMLSelectElement;
392 this.singleCommit = select.value;
393 this.dispatchRangeEvent();
394 }
395
396 /**
Philip Zeyligere89b3082025-05-29 03:16:06 +0000397 * Handle refresh button click
398 */
399 handleRefresh() {
400 this.loadCommits();
401 }
402
403 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700404 * Dispatch range change event
405 */
406 dispatchRangeEvent() {
Autoformatter8c463622025-05-16 21:54:17 +0000407 const range: DiffRange =
408 this.rangeType === "range"
409 ? { type: "range", from: this.fromCommit, to: this.toCommit }
410 : { type: "single", commit: this.singleCommit };
411
412 const event = new CustomEvent("range-change", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700413 detail: { range },
414 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000415 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700416 });
Autoformatter8c463622025-05-16 21:54:17 +0000417
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700418 this.dispatchEvent(event);
419 }
420}
421
422declare global {
423 interface HTMLElementTagNameMap {
424 "sketch-diff-range-picker": SketchDiffRangePicker;
425 }
426}