blob: cadafaecba7fb35b5f37098822f58c5c5003d1c6 [file] [log] [blame]
Philip Zeyliger272a90e2025-05-16 14:49:51 -07001// sketch-diff-file-picker.ts
2// Component for selecting files from a diff
3
4import { css, html, LitElement } from "lit";
5import { customElement, property, state } from "lit/decorators.js";
6import { GitDiffFile } from "./git-data-service";
7
8/**
9 * Component for selecting files from a diff with next/previous navigation
10 */
11@customElement("sketch-diff-file-picker")
12export class SketchDiffFilePicker extends LitElement {
13 @property({ type: Array })
14 files: GitDiffFile[] = [];
15
16 @property({ type: String })
17 selectedPath: string = "";
18
19 @state()
20 private selectedIndex: number = -1;
21
22 static styles = css`
23 :host {
24 display: block;
25 width: 100%;
26 font-family: var(--font-family, system-ui, sans-serif);
27 }
28
29 .file-picker {
30 display: flex;
31 gap: 8px;
32 align-items: center;
33 background-color: var(--background-light, #f8f8f8);
34 border-radius: 4px;
35 border: 1px solid var(--border-color, #e0e0e0);
36 padding: 8px 12px;
37 width: 100%;
38 box-sizing: border-box;
39 }
40
41 .file-select {
42 flex: 1;
43 min-width: 200px;
Autoformatter8c463622025-05-16 21:54:17 +000044 max-width: calc(
45 100% - 230px
46 ); /* Leave space for the navigation buttons and file info */
Philip Zeyliger272a90e2025-05-16 14:49:51 -070047 overflow: hidden;
48 }
49
50 select {
51 width: 100%;
52 max-width: 100%;
53 padding: 8px 12px;
54 border-radius: 4px;
55 border: 1px solid var(--border-color, #e0e0e0);
56 background-color: white;
57 font-size: 14px;
58 overflow: hidden;
59 text-overflow: ellipsis;
60 white-space: nowrap;
61 }
62
63 .navigation-buttons {
64 display: flex;
65 gap: 8px;
66 }
67
68 button {
69 padding: 8px 12px;
70 background-color: var(--button-bg, #4a7dfc);
71 color: var(--button-text, white);
72 border: none;
73 border-radius: 4px;
74 cursor: pointer;
75 font-size: 14px;
76 transition: background-color 0.2s;
77 }
78
79 button:hover {
80 background-color: var(--button-hover, #3a6eee);
81 }
82
83 button:disabled {
84 background-color: var(--button-disabled, #cccccc);
85 cursor: not-allowed;
86 }
87
Philip Zeyligere89b3082025-05-29 03:16:06 +000088 .file-position {
Philip Zeyliger272a90e2025-05-16 14:49:51 -070089 font-size: 14px;
90 color: var(--text-muted, #666);
Philip Zeyligere89b3082025-05-29 03:16:06 +000091 font-weight: 500;
92 padding: 0 12px;
93 display: flex;
94 align-items: center;
Philip Zeyliger272a90e2025-05-16 14:49:51 -070095 white-space: nowrap;
96 }
97
98 .no-files {
99 color: var(--text-muted, #666);
100 font-style: italic;
101 }
102
103 @media (max-width: 768px) {
104 .file-picker {
105 flex-direction: column;
106 align-items: stretch;
107 }
108
109 .file-select {
110 max-width: 100%; /* Full width on small screens */
111 margin-bottom: 8px;
112 }
113
114 .navigation-buttons {
115 width: 100%;
116 justify-content: space-between;
117 }
Autoformatter8c463622025-05-16 21:54:17 +0000118
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700119 .file-info {
120 margin-left: 0;
121 margin-top: 8px;
122 text-align: center;
123 }
124 }
125 `;
126
127 updated(changedProperties: Map<string, any>) {
128 // If files changed, reset the selection
Autoformatter8c463622025-05-16 21:54:17 +0000129 if (changedProperties.has("files")) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700130 this.updateSelectedIndex();
131 }
132
133 // If selectedPath changed externally, update the index
Autoformatter8c463622025-05-16 21:54:17 +0000134 if (changedProperties.has("selectedPath")) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700135 this.updateSelectedIndex();
136 }
137 }
Autoformatter8c463622025-05-16 21:54:17 +0000138
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700139 connectedCallback() {
140 super.connectedCallback();
141 // Initialize the selection when the component is connected, but only if files exist
142 if (this.files && this.files.length > 0) {
143 this.updateSelectedIndex();
Autoformatter8c463622025-05-16 21:54:17 +0000144
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700145 // Explicitly trigger file selection event for the first file when there's only one file
146 // This ensures the diff view is updated even when navigation buttons aren't clicked
147 if (this.files.length === 1) {
148 this.selectFileByIndex(0);
149 }
150 }
151 }
152
153 render() {
154 if (!this.files || this.files.length === 0) {
155 return html`<div class="no-files">No files to display</div>`;
156 }
157
158 return html`
159 <div class="file-picker">
160 <div class="file-select">
161 <select @change=${this.handleSelect}>
162 ${this.files.map(
163 (file, index) => html`
Autoformatter8c463622025-05-16 21:54:17 +0000164 <option
165 value=${index}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700166 ?selected=${index === this.selectedIndex}
167 >
168 ${this.formatFileOption(file)}
169 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000170 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700171 )}
172 </select>
173 </div>
Autoformatter8c463622025-05-16 21:54:17 +0000174
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700175 <div class="navigation-buttons">
Autoformatter8c463622025-05-16 21:54:17 +0000176 <button
177 @click=${this.handlePrevious}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700178 ?disabled=${this.selectedIndex <= 0}
179 >
180 Previous
181 </button>
Philip Zeyligere89b3082025-05-29 03:16:06 +0000182 ${this.selectedIndex >= 0 ? this.renderFilePosition() : ""}
Autoformatter8c463622025-05-16 21:54:17 +0000183 <button
184 @click=${this.handleNext}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700185 ?disabled=${this.selectedIndex >= this.files.length - 1}
186 >
187 Next
188 </button>
189 </div>
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700190 </div>
191 `;
192 }
193
Philip Zeyligere89b3082025-05-29 03:16:06 +0000194 renderFilePosition() {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700195 return html`
Philip Zeyligere89b3082025-05-29 03:16:06 +0000196 <div class="file-position">
197 ${this.selectedIndex + 1} of ${this.files.length}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700198 </div>
199 `;
200 }
201
202 /**
203 * Format a file for display in the dropdown
204 */
205 formatFileOption(file: GitDiffFile): string {
206 const statusSymbol = this.getFileStatusSymbol(file.status);
Philip Zeyligere89b3082025-05-29 03:16:06 +0000207 const changesInfo = this.getChangesInfo(file);
Josh Bleecher Snyderbcc1c412025-05-29 00:36:49 +0000208 const pathInfo = this.getPathInfo(file);
209 return `${statusSymbol} ${pathInfo}${changesInfo}`;
Philip Zeyligere89b3082025-05-29 03:16:06 +0000210 }
211
212 /**
213 * Get changes information (+/-) for display
214 */
215 getChangesInfo(file: GitDiffFile): string {
216 const additions = file.additions || 0;
217 const deletions = file.deletions || 0;
218
219 if (additions === 0 && deletions === 0) {
220 return "";
221 }
222
223 const parts = [];
224 if (additions > 0) {
225 parts.push(`+${additions}`);
226 }
227 if (deletions > 0) {
228 parts.push(`-${deletions}`);
229 }
230
231 return ` (${parts.join(", ")})`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700232 }
233
234 /**
Josh Bleecher Snyderbcc1c412025-05-29 00:36:49 +0000235 * Get path information for display, handling renames and copies
236 */
237 getPathInfo(file: GitDiffFile): string {
238 if (file.old_path && file.old_path !== "") {
239 // For renames and copies, show old_path -> new_path
240 return `${file.old_path} → ${file.path}`;
241 }
242 // For regular files, just show the path
243 return file.path;
244 }
245
246 /**
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700247 * Get a short symbol for the file status
248 */
249 getFileStatusSymbol(status: string): string {
250 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000251 case "A":
252 return "+";
253 case "M":
254 return "M";
255 case "D":
256 return "-";
257 case "R":
258 return "R";
259 default:
Josh Bleecher Snyderbcc1c412025-05-29 00:36:49 +0000260 // Handle copy statuses like C096, C100, etc.
261 if (status.toUpperCase().startsWith("C")) {
262 return "C";
263 }
264 // Handle rename statuses like R096, R100, etc.
265 if (status.toUpperCase().startsWith("R")) {
266 return "R";
267 }
Autoformatter8c463622025-05-16 21:54:17 +0000268 return "?";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700269 }
270 }
271
272 /**
273 * Get a descriptive name for the file status
274 */
275 getFileStatusName(status: string): string {
276 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000277 case "A":
278 return "Added";
279 case "M":
280 return "Modified";
281 case "D":
282 return "Deleted";
283 case "R":
284 return "Renamed";
285 default:
Josh Bleecher Snyderbcc1c412025-05-29 00:36:49 +0000286 // Handle copy statuses like C096, C100, etc.
287 if (status.toUpperCase().startsWith("C")) {
288 return "Copied";
289 }
290 // Handle rename statuses like R096, R100, etc.
291 if (status.toUpperCase().startsWith("R")) {
292 return "Renamed";
293 }
Autoformatter8c463622025-05-16 21:54:17 +0000294 return "Unknown";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700295 }
296 }
297
298 /**
299 * Handle file selection from dropdown
300 */
301 handleSelect(event: Event) {
302 const select = event.target as HTMLSelectElement;
303 const index = parseInt(select.value, 10);
304 this.selectFileByIndex(index);
305 }
306
307 /**
308 * Handle previous button click
309 */
310 handlePrevious() {
311 if (this.selectedIndex > 0) {
312 this.selectFileByIndex(this.selectedIndex - 1);
313 }
314 }
315
316 /**
317 * Handle next button click
318 */
319 handleNext() {
320 if (this.selectedIndex < this.files.length - 1) {
321 this.selectFileByIndex(this.selectedIndex + 1);
322 }
323 }
324
325 /**
326 * Select a file by index and dispatch event
327 */
328 selectFileByIndex(index: number) {
329 if (index >= 0 && index < this.files.length) {
330 this.selectedIndex = index;
331 this.selectedPath = this.files[index].path;
Autoformatter8c463622025-05-16 21:54:17 +0000332
333 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700334 detail: { file: this.files[index] },
335 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000336 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700337 });
Autoformatter8c463622025-05-16 21:54:17 +0000338
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700339 this.dispatchEvent(event);
340 }
341 }
342
343 /**
344 * Update the selected index based on the selectedPath
345 */
346 private updateSelectedIndex() {
347 // Add defensive check for files array
348 if (!this.files || this.files.length === 0) {
349 this.selectedIndex = -1;
350 return;
351 }
352
353 if (this.selectedPath) {
354 // Find the file with the matching path
Autoformatter8c463622025-05-16 21:54:17 +0000355 const index = this.files.findIndex(
356 (file) => file.path === this.selectedPath,
357 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700358 if (index >= 0) {
359 this.selectedIndex = index;
360 return;
361 }
362 }
363
364 // Default to first file if no match or no path
365 this.selectedIndex = 0;
366 const newSelectedPath = this.files[0].path;
Autoformatter8c463622025-05-16 21:54:17 +0000367
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700368 // Only dispatch event if the path has actually changed and files exist
Autoformatter8c463622025-05-16 21:54:17 +0000369 if (
370 this.selectedPath !== newSelectedPath &&
371 this.files &&
372 this.files.length > 0
373 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700374 this.selectedPath = newSelectedPath;
Autoformatter8c463622025-05-16 21:54:17 +0000375
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700376 // Dispatch the event directly - we've already checked the files array
Autoformatter8c463622025-05-16 21:54:17 +0000377 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700378 detail: { file: this.files[0] },
379 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000380 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700381 });
Autoformatter8c463622025-05-16 21:54:17 +0000382
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700383 this.dispatchEvent(event);
384 }
385 }
386}
387
388declare global {
389 interface HTMLElementTagNameMap {
390 "sketch-diff-file-picker": SketchDiffFilePicker;
391 }
392}