blob: 9893875f328d8a4743602899a83d4598a8fff45c [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 */
12export type DiffRange =
13 | { type: 'range'; from: string; to: string }
14 | { type: 'single'; commit: string };
15
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()
25 private rangeType: 'range' | 'single' = 'range';
26
27 @state()
28 private fromCommit: string = '';
29
30 @state()
31 private toCommit: string = '';
32
33 @state()
34 private singleCommit: string = '';
35
36 @state()
37 private loading: boolean = true;
38
39 @state()
40 private error: string | null = null;
41
42 @property({ attribute: false, type: Object })
43 gitService!: GitDataService;
44
45 constructor() {
46 super();
47 console.log('SketchDiffRangePicker initialized');
48 }
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 }
130
131 @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
141 if (document.readyState === 'complete') {
142 this.loadCommits();
143 } else {
144 window.addEventListener('load', () => {
145 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
156 ? html`<div class="error">${this.error}</div>`
157 : this.renderRangePicker()
158 }
159 </div>
160 `;
161 }
162
163 renderRangePicker() {
164 return html`
165 <div class="range-type-selector">
166 <label class="range-type-option">
167 <input
168 type="radio"
169 name="rangeType"
170 value="range"
171 ?checked=${this.rangeType === 'range'}
172 @change=${() => this.setRangeType('range')}
173 />
174 Commit Range
175 </label>
176 <label class="range-type-option">
177 <input
178 type="radio"
179 name="rangeType"
180 value="single"
181 ?checked=${this.rangeType === 'single'}
182 @change=${() => this.setRangeType('single')}
183 />
184 Single Commit
185 </label>
186 </div>
187
188 <div class="commit-selectors">
189 ${this.rangeType === 'range'
190 ? this.renderRangeSelectors()
191 : this.renderSingleSelector()
192 }
193 </div>
194 `;
195 }
196
197 renderRangeSelectors() {
198 return html`
199 <div class="commit-selector">
200 <label for="fromCommit">From:</label>
201 <select
202 id="fromCommit"
203 .value=${this.fromCommit}
204 @change=${this.handleFromChange}
205 >
206 ${this.commits.map(
207 commit => html`
208 <option value=${commit.hash} ?selected=${commit.hash === this.fromCommit}>
209 ${this.formatCommitOption(commit)}
210 </option>
211 `
212 )}
213 </select>
214 </div>
215 <div class="commit-selector">
216 <label for="toCommit">To:</label>
217 <select
218 id="toCommit"
219 .value=${this.toCommit}
220 @change=${this.handleToChange}
221 >
222 <option value="" ?selected=${this.toCommit === ''}>Uncommitted Changes</option>
223 ${this.commits.map(
224 commit => html`
225 <option value=${commit.hash} ?selected=${commit.hash === this.toCommit}>
226 ${this.formatCommitOption(commit)}
227 </option>
228 `
229 )}
230 </select>
231 </div>
232 `;
233 }
234
235 renderSingleSelector() {
236 return html`
237 <div class="commit-selector">
238 <label for="singleCommit">Commit:</label>
239 <select
240 id="singleCommit"
241 .value=${this.singleCommit}
242 @change=${this.handleSingleChange}
243 >
244 ${this.commits.map(
245 commit => html`
246 <option value=${commit.hash} ?selected=${commit.hash === this.singleCommit}>
247 ${this.formatCommitOption(commit)}
248 </option>
249 `
250 )}
251 </select>
252 </div>
253 `;
254 }
255
256 /**
257 * Format a commit for display in the dropdown
258 */
259 formatCommitOption(commit: GitLogEntry): string {
260 const shortHash = commit.hash.substring(0, 7);
261 let label = `${shortHash} ${commit.subject}`;
262
263 if (commit.refs && commit.refs.length > 0) {
264 label += ` (${commit.refs.join(', ')})`;
265 }
266
267 return label;
268 }
269
270 /**
271 * Load commits from the Git data service
272 */
273 async loadCommits() {
274 this.loading = true;
275 this.error = null;
276
277 if (!this.gitService) {
278 console.error('GitService was not provided to sketch-diff-range-picker');
279 throw Error();
280 }
281
282 try {
283 // Get the base commit reference
284 const baseCommitRef = await this.gitService.getBaseCommitRef();
285
286 // Load commit history
287 this.commits = await this.gitService.getCommitHistory(baseCommitRef);
288
289 // Set default selections
290 if (this.commits.length > 0) {
291 // For range, default is base to HEAD
292 // TODO: is sketch-base right in the unsafe context, where it's sketch-base-...
293 // should this be startswith?
294 const baseCommit = this.commits.find(c =>
295 c.refs && c.refs.some(ref => ref.includes('sketch-base'))
296 );
297
298 this.fromCommit = baseCommit ? baseCommit.hash : this.commits[this.commits.length - 1].hash;
299 // Default to Uncommitted Changes by setting toCommit to empty string
300 this.toCommit = ''; // Empty string represents uncommitted changes
301
302 // For single, default to HEAD
303 this.singleCommit = this.commits[0].hash;
304
305 // Dispatch initial range event
306 this.dispatchRangeEvent();
307 }
308 } catch (error) {
309 console.error('Error loading commits:', error);
310 this.error = `Error loading commits: ${error.message}`;
311 } finally {
312 this.loading = false;
313 }
314 }
315
316 /**
317 * Handle range type change
318 */
319 setRangeType(type: 'range' | 'single') {
320 this.rangeType = type;
321 this.dispatchRangeEvent();
322 }
323
324 /**
325 * Handle From commit change
326 */
327 handleFromChange(event: Event) {
328 const select = event.target as HTMLSelectElement;
329 this.fromCommit = select.value;
330 this.dispatchRangeEvent();
331 }
332
333 /**
334 * Handle To commit change
335 */
336 handleToChange(event: Event) {
337 const select = event.target as HTMLSelectElement;
338 this.toCommit = select.value;
339 this.dispatchRangeEvent();
340 }
341
342 /**
343 * Handle Single commit change
344 */
345 handleSingleChange(event: Event) {
346 const select = event.target as HTMLSelectElement;
347 this.singleCommit = select.value;
348 this.dispatchRangeEvent();
349 }
350
351 /**
352 * Dispatch range change event
353 */
354 dispatchRangeEvent() {
355 const range: DiffRange = this.rangeType === 'range'
356 ? { type: 'range', from: this.fromCommit, to: this.toCommit }
357 : { type: 'single', commit: this.singleCommit };
358
359 const event = new CustomEvent('range-change', {
360 detail: { range },
361 bubbles: true,
362 composed: true
363 });
364
365 this.dispatchEvent(event);
366 }
367}
368
369declare global {
370 interface HTMLElementTagNameMap {
371 "sketch-diff-range-picker": SketchDiffRangePicker;
372 }
373}