blob: c834f129e8d786eedffc4094198a7ed7a3df099a [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
88 .file-info {
89 font-size: 14px;
90 color: var(--text-muted, #666);
91 margin-left: 8px;
92 white-space: nowrap;
93 }
94
95 .no-files {
96 color: var(--text-muted, #666);
97 font-style: italic;
98 }
99
100 @media (max-width: 768px) {
101 .file-picker {
102 flex-direction: column;
103 align-items: stretch;
104 }
105
106 .file-select {
107 max-width: 100%; /* Full width on small screens */
108 margin-bottom: 8px;
109 }
110
111 .navigation-buttons {
112 width: 100%;
113 justify-content: space-between;
114 }
Autoformatter8c463622025-05-16 21:54:17 +0000115
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700116 .file-info {
117 margin-left: 0;
118 margin-top: 8px;
119 text-align: center;
120 }
121 }
122 `;
123
124 updated(changedProperties: Map<string, any>) {
125 // If files changed, reset the selection
Autoformatter8c463622025-05-16 21:54:17 +0000126 if (changedProperties.has("files")) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700127 this.updateSelectedIndex();
128 }
129
130 // If selectedPath changed externally, update the index
Autoformatter8c463622025-05-16 21:54:17 +0000131 if (changedProperties.has("selectedPath")) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700132 this.updateSelectedIndex();
133 }
134 }
Autoformatter8c463622025-05-16 21:54:17 +0000135
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700136 connectedCallback() {
137 super.connectedCallback();
138 // Initialize the selection when the component is connected, but only if files exist
139 if (this.files && this.files.length > 0) {
140 this.updateSelectedIndex();
Autoformatter8c463622025-05-16 21:54:17 +0000141
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700142 // Explicitly trigger file selection event for the first file when there's only one file
143 // This ensures the diff view is updated even when navigation buttons aren't clicked
144 if (this.files.length === 1) {
145 this.selectFileByIndex(0);
146 }
147 }
148 }
149
150 render() {
151 if (!this.files || this.files.length === 0) {
152 return html`<div class="no-files">No files to display</div>`;
153 }
154
155 return html`
156 <div class="file-picker">
157 <div class="file-select">
158 <select @change=${this.handleSelect}>
159 ${this.files.map(
160 (file, index) => html`
Autoformatter8c463622025-05-16 21:54:17 +0000161 <option
162 value=${index}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700163 ?selected=${index === this.selectedIndex}
164 >
165 ${this.formatFileOption(file)}
166 </option>
Autoformatter8c463622025-05-16 21:54:17 +0000167 `,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700168 )}
169 </select>
170 </div>
Autoformatter8c463622025-05-16 21:54:17 +0000171
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700172 <div class="navigation-buttons">
Autoformatter8c463622025-05-16 21:54:17 +0000173 <button
174 @click=${this.handlePrevious}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700175 ?disabled=${this.selectedIndex <= 0}
176 >
177 Previous
178 </button>
Autoformatter8c463622025-05-16 21:54:17 +0000179 <button
180 @click=${this.handleNext}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700181 ?disabled=${this.selectedIndex >= this.files.length - 1}
182 >
183 Next
184 </button>
185 </div>
186
Autoformatter8c463622025-05-16 21:54:17 +0000187 ${this.selectedIndex >= 0 ? this.renderFileInfo() : ""}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700188 </div>
189 `;
190 }
191
192 renderFileInfo() {
193 const file = this.files[this.selectedIndex];
194 return html`
195 <div class="file-info">
Autoformatter8c463622025-05-16 21:54:17 +0000196 ${this.getFileStatusName(file.status)} | ${this.selectedIndex + 1} of
197 ${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);
207 return `${statusSymbol} ${file.path}`;
208 }
209
210 /**
211 * Get a short symbol for the file status
212 */
213 getFileStatusSymbol(status: string): string {
214 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000215 case "A":
216 return "+";
217 case "M":
218 return "M";
219 case "D":
220 return "-";
221 case "R":
222 return "R";
223 default:
224 return "?";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700225 }
226 }
227
228 /**
229 * Get a descriptive name for the file status
230 */
231 getFileStatusName(status: string): string {
232 switch (status.toUpperCase()) {
Autoformatter8c463622025-05-16 21:54:17 +0000233 case "A":
234 return "Added";
235 case "M":
236 return "Modified";
237 case "D":
238 return "Deleted";
239 case "R":
240 return "Renamed";
241 default:
242 return "Unknown";
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700243 }
244 }
245
246 /**
247 * Handle file selection from dropdown
248 */
249 handleSelect(event: Event) {
250 const select = event.target as HTMLSelectElement;
251 const index = parseInt(select.value, 10);
252 this.selectFileByIndex(index);
253 }
254
255 /**
256 * Handle previous button click
257 */
258 handlePrevious() {
259 if (this.selectedIndex > 0) {
260 this.selectFileByIndex(this.selectedIndex - 1);
261 }
262 }
263
264 /**
265 * Handle next button click
266 */
267 handleNext() {
268 if (this.selectedIndex < this.files.length - 1) {
269 this.selectFileByIndex(this.selectedIndex + 1);
270 }
271 }
272
273 /**
274 * Select a file by index and dispatch event
275 */
276 selectFileByIndex(index: number) {
277 if (index >= 0 && index < this.files.length) {
278 this.selectedIndex = index;
279 this.selectedPath = this.files[index].path;
Autoformatter8c463622025-05-16 21:54:17 +0000280
281 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700282 detail: { file: this.files[index] },
283 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000284 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700285 });
Autoformatter8c463622025-05-16 21:54:17 +0000286
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700287 this.dispatchEvent(event);
288 }
289 }
290
291 /**
292 * Update the selected index based on the selectedPath
293 */
294 private updateSelectedIndex() {
295 // Add defensive check for files array
296 if (!this.files || this.files.length === 0) {
297 this.selectedIndex = -1;
298 return;
299 }
300
301 if (this.selectedPath) {
302 // Find the file with the matching path
Autoformatter8c463622025-05-16 21:54:17 +0000303 const index = this.files.findIndex(
304 (file) => file.path === this.selectedPath,
305 );
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700306 if (index >= 0) {
307 this.selectedIndex = index;
308 return;
309 }
310 }
311
312 // Default to first file if no match or no path
313 this.selectedIndex = 0;
314 const newSelectedPath = this.files[0].path;
Autoformatter8c463622025-05-16 21:54:17 +0000315
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700316 // Only dispatch event if the path has actually changed and files exist
Autoformatter8c463622025-05-16 21:54:17 +0000317 if (
318 this.selectedPath !== newSelectedPath &&
319 this.files &&
320 this.files.length > 0
321 ) {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700322 this.selectedPath = newSelectedPath;
Autoformatter8c463622025-05-16 21:54:17 +0000323
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700324 // Dispatch the event directly - we've already checked the files array
Autoformatter8c463622025-05-16 21:54:17 +0000325 const event = new CustomEvent("file-selected", {
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700326 detail: { file: this.files[0] },
327 bubbles: true,
Autoformatter8c463622025-05-16 21:54:17 +0000328 composed: true,
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700329 });
Autoformatter8c463622025-05-16 21:54:17 +0000330
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700331 this.dispatchEvent(event);
332 }
333 }
334}
335
336declare global {
337 interface HTMLElementTagNameMap {
338 "sketch-diff-file-picker": SketchDiffFilePicker;
339 }
340}