blob: 70b09f72037b3fc795b78841b5d01642cbee6f1e [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 Zeyliger272a90e2025-05-16 14:49:51 -0700131 @media (max-width: 768px) {
132 .commit-selector {
133 max-width: 100%;
134 }
135 }
136 `;
137
138 connectedCallback() {
139 super.connectedCallback();
140 // Wait for DOM to be fully loaded to ensure proper initialization order
Autoformatter8c463622025-05-16 21:54:17 +0000141 if (document.readyState === "complete") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700142 this.loadCommits();
143 } else {
Autoformatter8c463622025-05-16 21:54:17 +0000144 window.addEventListener("load", () => {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700145 setTimeout(() => this.loadCommits(), 0); // Give time for provider initialization
146 });
147 }
148 }
149
150 render() {
151 return html`
152 <div class="range-picker">
153 ${this.loading
154 ? html`<div class="loading">Loading commits...</div>`
155 : this.error
Autoformatter8c463622025-05-16 21:54:17 +0000156 ? html`<div class="error">${this.error}</div>`
157 : this.renderRangePicker()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700158 </div>
159 `;
160 }
161
162 renderRangePicker() {
163 return html`
164 <div class="range-type-selector">
165 <label class="range-type-option">
166 <input
167 type="radio"
168 name="rangeType"
169 value="range"
Autoformatter8c463622025-05-16 21:54:17 +0000170 ?checked=${this.rangeType === "range"}
171 @change=${() => this.setRangeType("range")}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700172 />
173 Commit Range
174 </label>
175 <label class="range-type-option">
176 <input
177 type="radio"
178 name="rangeType"
179 value="single"
Autoformatter8c463622025-05-16 21:54:17 +0000180 ?checked=${this.rangeType === "single"}
181 @change=${() => this.setRangeType("single")}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700182 />
183 Single Commit
184 </label>
185 </div>
186
187 <div class="commit-selectors">
Autoformatter8c463622025-05-16 21:54:17 +0000188 ${this.rangeType === "range"
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700189 ? this.renderRangeSelectors()
Autoformatter8c463622025-05-16 21:54:17 +0000190 : this.renderSingleSelector()}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700191 </div>
192 `;
193 }
194
195 renderRangeSelectors() {
196 return html`
197 <div class="commit-selector">
198 <label for="fromCommit">From:</label>
199 <select
200 id="fromCommit"
201 .value=${this.fromCommit}
202 @change=${this.handleFromChange}
203 >
204 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000205 (commit) => html`
206 <option
207 value=${commit.hash}
208 ?selected=${commit.hash === this.fromCommit}
209 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700210 ${this.formatCommitOption(commit)}
211 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000212 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700213 )}
214 </select>
215 </div>
216 <div class="commit-selector">
217 <label for="toCommit">To:</label>
218 <select
219 id="toCommit"
220 .value=${this.toCommit}
221 @change=${this.handleToChange}
222 >
Autoformatter8c463622025-05-16 21:54:17 +0000223 <option value="" ?selected=${this.toCommit === ""}>
224 Uncommitted Changes
225 </option>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700226 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000227 (commit) => html`
228 <option
229 value=${commit.hash}
230 ?selected=${commit.hash === this.toCommit}
231 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700232 ${this.formatCommitOption(commit)}
233 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000234 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700235 )}
236 </select>
237 </div>
238 `;
239 }
240
241 renderSingleSelector() {
242 return html`
243 <div class="commit-selector">
244 <label for="singleCommit">Commit:</label>
245 <select
246 id="singleCommit"
247 .value=${this.singleCommit}
248 @change=${this.handleSingleChange}
249 >
250 ${this.commits.map(
Autoformatter8c463622025-05-16 21:54:17 +0000251 (commit) => html`
252 <option
253 value=${commit.hash}
254 ?selected=${commit.hash === this.singleCommit}
255 >
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700256 ${this.formatCommitOption(commit)}
257 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000258 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700259 )}
260 </select>
261 </div>
262 `;
263 }
264
265 /**
266 * Format a commit for display in the dropdown
267 */
268 formatCommitOption(commit: GitLogEntry): string {
269 const shortHash = commit.hash.substring(0, 7);
270 let label = `${shortHash} ${commit.subject}`;
Autoformatter8c463622025-05-16 21:54:17 +0000271
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700272 if (commit.refs && commit.refs.length > 0) {
Autoformatter8c463622025-05-16 21:54:17 +0000273 label += ` (${commit.refs.join(", ")})`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700274 }
Autoformatter8c463622025-05-16 21:54:17 +0000275
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700276 return label;
277 }
278
279 /**
280 * Load commits from the Git data service
281 */
282 async loadCommits() {
283 this.loading = true;
284 this.error = null;
285
286 if (!this.gitService) {
Autoformatter8c463622025-05-16 21:54:17 +0000287 console.error("GitService was not provided to sketch-diff-range-picker");
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700288 throw Error();
289 }
290
291 try {
292 // Get the base commit reference
293 const baseCommitRef = await this.gitService.getBaseCommitRef();
Autoformatter8c463622025-05-16 21:54:17 +0000294
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700295 // Load commit history
296 this.commits = await this.gitService.getCommitHistory(baseCommitRef);
Autoformatter8c463622025-05-16 21:54:17 +0000297
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700298 // Set default selections
299 if (this.commits.length > 0) {
300 // For range, default is base to HEAD
301 // TODO: is sketch-base right in the unsafe context, where it's sketch-base-...
302 // should this be startswith?
Autoformatter8c463622025-05-16 21:54:17 +0000303 const baseCommit = this.commits.find(
304 (c) => c.refs && c.refs.some((ref) => ref.includes("sketch-base")),
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700305 );
Autoformatter8c463622025-05-16 21:54:17 +0000306
307 this.fromCommit = baseCommit
308 ? baseCommit.hash
309 : this.commits[this.commits.length - 1].hash;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700310 // Default to Uncommitted Changes by setting toCommit to empty string
Autoformatter8c463622025-05-16 21:54:17 +0000311 this.toCommit = ""; // Empty string represents uncommitted changes
312
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700313 // For single, default to HEAD
314 this.singleCommit = this.commits[0].hash;
Autoformatter8c463622025-05-16 21:54:17 +0000315
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700316 // Dispatch initial range event
317 this.dispatchRangeEvent();
318 }
319 } catch (error) {
Autoformatter8c463622025-05-16 21:54:17 +0000320 console.error("Error loading commits:", error);
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700321 this.error = `Error loading commits: ${error.message}`;
322 } finally {
323 this.loading = false;
324 }
325 }
326
327 /**
328 * Handle range type change
329 */
Autoformatter8c463622025-05-16 21:54:17 +0000330 setRangeType(type: "range" | "single") {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700331 this.rangeType = type;
332 this.dispatchRangeEvent();
333 }
334
335 /**
336 * Handle From commit change
337 */
338 handleFromChange(event: Event) {
339 const select = event.target as HTMLSelectElement;
340 this.fromCommit = select.value;
341 this.dispatchRangeEvent();
342 }
343
344 /**
345 * Handle To commit change
346 */
347 handleToChange(event: Event) {
348 const select = event.target as HTMLSelectElement;
349 this.toCommit = select.value;
350 this.dispatchRangeEvent();
351 }
352
353 /**
354 * Handle Single commit change
355 */
356 handleSingleChange(event: Event) {
357 const select = event.target as HTMLSelectElement;
358 this.singleCommit = select.value;
359 this.dispatchRangeEvent();
360 }
361
362 /**
363 * Dispatch range change event
364 */
365 dispatchRangeEvent() {
Autoformatter8c463622025-05-16 21:54:17 +0000366 const range: DiffRange =
367 this.rangeType === "range"
368 ? { type: "range", from: this.fromCommit, to: this.toCommit }
369 : { type: "single", commit: this.singleCommit };
370
371 const event = new CustomEvent("range-change", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700372 detail: { range },
373 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000374 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700375 });
Autoformatter8c463622025-05-16 21:54:17 +0000376
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700377 this.dispatchEvent(event);
378 }
379}
380
381declare global {
382 interface HTMLElementTagNameMap {
383 "sketch-diff-range-picker": SketchDiffRangePicker;
384 }
385}