blob: c7978eef3ddfcac911763413b3d368e8ce2e1cd8 [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;
44 max-width: calc(100% - 230px); /* Leave space for the navigation buttons and file info */
45 overflow: hidden;
46 }
47
48 select {
49 width: 100%;
50 max-width: 100%;
51 padding: 8px 12px;
52 border-radius: 4px;
53 border: 1px solid var(--border-color, #e0e0e0);
54 background-color: white;
55 font-size: 14px;
56 overflow: hidden;
57 text-overflow: ellipsis;
58 white-space: nowrap;
59 }
60
61 .navigation-buttons {
62 display: flex;
63 gap: 8px;
64 }
65
66 button {
67 padding: 8px 12px;
68 background-color: var(--button-bg, #4a7dfc);
69 color: var(--button-text, white);
70 border: none;
71 border-radius: 4px;
72 cursor: pointer;
73 font-size: 14px;
74 transition: background-color 0.2s;
75 }
76
77 button:hover {
78 background-color: var(--button-hover, #3a6eee);
79 }
80
81 button:disabled {
82 background-color: var(--button-disabled, #cccccc);
83 cursor: not-allowed;
84 }
85
86 .file-info {
87 font-size: 14px;
88 color: var(--text-muted, #666);
89 margin-left: 8px;
90 white-space: nowrap;
91 }
92
93 .no-files {
94 color: var(--text-muted, #666);
95 font-style: italic;
96 }
97
98 @media (max-width: 768px) {
99 .file-picker {
100 flex-direction: column;
101 align-items: stretch;
102 }
103
104 .file-select {
105 max-width: 100%; /* Full width on small screens */
106 margin-bottom: 8px;
107 }
108
109 .navigation-buttons {
110 width: 100%;
111 justify-content: space-between;
112 }
113
114 .file-info {
115 margin-left: 0;
116 margin-top: 8px;
117 text-align: center;
118 }
119 }
120 `;
121
122 updated(changedProperties: Map<string, any>) {
123 // If files changed, reset the selection
124 if (changedProperties.has('files')) {
125 this.updateSelectedIndex();
126 }
127
128 // If selectedPath changed externally, update the index
129 if (changedProperties.has('selectedPath')) {
130 this.updateSelectedIndex();
131 }
132 }
133
134 connectedCallback() {
135 super.connectedCallback();
136 // Initialize the selection when the component is connected, but only if files exist
137 if (this.files && this.files.length > 0) {
138 this.updateSelectedIndex();
139
140 // Explicitly trigger file selection event for the first file when there's only one file
141 // This ensures the diff view is updated even when navigation buttons aren't clicked
142 if (this.files.length === 1) {
143 this.selectFileByIndex(0);
144 }
145 }
146 }
147
148 render() {
149 if (!this.files || this.files.length === 0) {
150 return html`<div class="no-files">No files to display</div>`;
151 }
152
153 return html`
154 <div class="file-picker">
155 <div class="file-select">
156 <select @change=${this.handleSelect}>
157 ${this.files.map(
158 (file, index) => html`
159 <option
160 value=${index}
161 ?selected=${index === this.selectedIndex}
162 >
163 ${this.formatFileOption(file)}
164 </option>
165 `
166 )}
167 </select>
168 </div>
169
170 <div class="navigation-buttons">
171 <button
172 @click=${this.handlePrevious}
173 ?disabled=${this.selectedIndex <= 0}
174 >
175 Previous
176 </button>
177 <button
178 @click=${this.handleNext}
179 ?disabled=${this.selectedIndex >= this.files.length - 1}
180 >
181 Next
182 </button>
183 </div>
184
185 ${this.selectedIndex >= 0 ? this.renderFileInfo() : ''}
186 </div>
187 `;
188 }
189
190 renderFileInfo() {
191 const file = this.files[this.selectedIndex];
192 return html`
193 <div class="file-info">
194 ${this.getFileStatusName(file.status)} |
195 ${this.selectedIndex + 1} of ${this.files.length}
196 </div>
197 `;
198 }
199
200 /**
201 * Format a file for display in the dropdown
202 */
203 formatFileOption(file: GitDiffFile): string {
204 const statusSymbol = this.getFileStatusSymbol(file.status);
205 return `${statusSymbol} ${file.path}`;
206 }
207
208 /**
209 * Get a short symbol for the file status
210 */
211 getFileStatusSymbol(status: string): string {
212 switch (status.toUpperCase()) {
213 case 'A': return '+';
214 case 'M': return 'M';
215 case 'D': return '-';
216 case 'R': return 'R';
217 default: return '?';
218 }
219 }
220
221 /**
222 * Get a descriptive name for the file status
223 */
224 getFileStatusName(status: string): string {
225 switch (status.toUpperCase()) {
226 case 'A': return 'Added';
227 case 'M': return 'Modified';
228 case 'D': return 'Deleted';
229 case 'R': return 'Renamed';
230 default: return 'Unknown';
231 }
232 }
233
234 /**
235 * Handle file selection from dropdown
236 */
237 handleSelect(event: Event) {
238 const select = event.target as HTMLSelectElement;
239 const index = parseInt(select.value, 10);
240 this.selectFileByIndex(index);
241 }
242
243 /**
244 * Handle previous button click
245 */
246 handlePrevious() {
247 if (this.selectedIndex > 0) {
248 this.selectFileByIndex(this.selectedIndex - 1);
249 }
250 }
251
252 /**
253 * Handle next button click
254 */
255 handleNext() {
256 if (this.selectedIndex < this.files.length - 1) {
257 this.selectFileByIndex(this.selectedIndex + 1);
258 }
259 }
260
261 /**
262 * Select a file by index and dispatch event
263 */
264 selectFileByIndex(index: number) {
265 if (index >= 0 && index < this.files.length) {
266 this.selectedIndex = index;
267 this.selectedPath = this.files[index].path;
268
269 const event = new CustomEvent('file-selected', {
270 detail: { file: this.files[index] },
271 bubbles: true,
272 composed: true
273 });
274
275 this.dispatchEvent(event);
276 }
277 }
278
279 /**
280 * Update the selected index based on the selectedPath
281 */
282 private updateSelectedIndex() {
283 // Add defensive check for files array
284 if (!this.files || this.files.length === 0) {
285 this.selectedIndex = -1;
286 return;
287 }
288
289 if (this.selectedPath) {
290 // Find the file with the matching path
291 const index = this.files.findIndex(file => file.path === this.selectedPath);
292 if (index >= 0) {
293 this.selectedIndex = index;
294 return;
295 }
296 }
297
298 // Default to first file if no match or no path
299 this.selectedIndex = 0;
300 const newSelectedPath = this.files[0].path;
301
302 // Only dispatch event if the path has actually changed and files exist
303 if (this.selectedPath !== newSelectedPath && this.files && this.files.length > 0) {
304 this.selectedPath = newSelectedPath;
305
306 // Dispatch the event directly - we've already checked the files array
307 const event = new CustomEvent('file-selected', {
308 detail: { file: this.files[0] },
309 bubbles: true,
310 composed: true
311 });
312
313 this.dispatchEvent(event);
314 }
315 }
316}
317
318declare global {
319 interface HTMLElementTagNameMap {
320 "sketch-diff-file-picker": SketchDiffFilePicker;
321 }
322}