blob: eb94a5f8107729bb8e67bd5ac1d3d44404b908bc [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);
208 return `${statusSymbol} ${file.path}${changesInfo}`;
209 }
210
211 /**
212 * Get changes information (+/-) for display
213 */
214 getChangesInfo(file: GitDiffFile): string {
215 const additions = file.additions || 0;
216 const deletions = file.deletions || 0;
217
218 if (additions === 0 && deletions === 0) {
219 return "";
220 }
221
222 const parts = [];
223 if (additions > 0) {
224 parts.push(`+${additions}`);
225 }
226 if (deletions > 0) {
227 parts.push(`-${deletions}`);
228 }
229
230 return ` (${parts.join(", ")})`;
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700231 }
232
233 /**
234 * Get a short symbol for the file status
235 */
236 getFileStatusSymbol(status: string): string {
237 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000238 case "A":
239 return "+";
240 case "M":
241 return "M";
242 case "D":
243 return "-";
244 case "R":
245 return "R";
246 default:
247 return "?";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700248 }
249 }
250
251 /**
252 * Get a descriptive name for the file status
253 */
254 getFileStatusName(status: string): string {
255 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000256 case "A":
257 return "Added";
258 case "M":
259 return "Modified";
260 case "D":
261 return "Deleted";
262 case "R":
263 return "Renamed";
264 default:
265 return "Unknown";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700266 }
267 }
268
269 /**
270 * Handle file selection from dropdown
271 */
272 handleSelect(event: Event) {
273 const select = event.target as HTMLSelectElement;
274 const index = parseInt(select.value, 10);
275 this.selectFileByIndex(index);
276 }
277
278 /**
279 * Handle previous button click
280 */
281 handlePrevious() {
282 if (this.selectedIndex > 0) {
283 this.selectFileByIndex(this.selectedIndex - 1);
284 }
285 }
286
287 /**
288 * Handle next button click
289 */
290 handleNext() {
291 if (this.selectedIndex < this.files.length - 1) {
292 this.selectFileByIndex(this.selectedIndex + 1);
293 }
294 }
295
296 /**
297 * Select a file by index and dispatch event
298 */
299 selectFileByIndex(index: number) {
300 if (index >= 0 && index < this.files.length) {
301 this.selectedIndex = index;
302 this.selectedPath = this.files[index].path;
Autoformatter8c463622025-05-16 21:54:17 +0000303
304 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700305 detail: { file: this.files[index] },
306 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000307 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700308 });
Autoformatter8c463622025-05-16 21:54:17 +0000309
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700310 this.dispatchEvent(event);
311 }
312 }
313
314 /**
315 * Update the selected index based on the selectedPath
316 */
317 private updateSelectedIndex() {
318 // Add defensive check for files array
319 if (!this.files || this.files.length === 0) {
320 this.selectedIndex = -1;
321 return;
322 }
323
324 if (this.selectedPath) {
325 // Find the file with the matching path
Autoformatter8c463622025-05-16 21:54:17 +0000326 const index = this.files.findIndex(
327 (file) => file.path === this.selectedPath,
328 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700329 if (index >= 0) {
330 this.selectedIndex = index;
331 return;
332 }
333 }
334
335 // Default to first file if no match or no path
336 this.selectedIndex = 0;
337 const newSelectedPath = this.files[0].path;
Autoformatter8c463622025-05-16 21:54:17 +0000338
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700339 // Only dispatch event if the path has actually changed and files exist
Autoformatter8c463622025-05-16 21:54:17 +0000340 if (
341 this.selectedPath !== newSelectedPath &&
342 this.files &&
343 this.files.length > 0
344 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700345 this.selectedPath = newSelectedPath;
Autoformatter8c463622025-05-16 21:54:17 +0000346
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700347 // Dispatch the event directly - we've already checked the files array
Autoformatter8c463622025-05-16 21:54:17 +0000348 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700349 detail: { file: this.files[0] },
350 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000351 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700352 });
Autoformatter8c463622025-05-16 21:54:17 +0000353
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700354 this.dispatchEvent(event);
355 }
356 }
357}
358
359declare global {
360 interface HTMLElementTagNameMap {
361 "sketch-diff-file-picker": SketchDiffFilePicker;
362 }
363}