webui: display file copy status (vs modify/rename) in diff view
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8e76980f4dd17de3k
diff --git a/webui/src/web-components/demo/mock-git-data-service.ts b/webui/src/web-components/demo/mock-git-data-service.ts
index 046d9ec..12834b3 100644
--- a/webui/src/web-components/demo/mock-git-data-service.ts
+++ b/webui/src/web-components/demo/mock-git-data-service.ts
@@ -76,6 +76,28 @@
deletions: 3,
},
{
+ path: "src/components/DialogPicker.js",
+ old_path: "src/components/FilePicker.js",
+ status: "C85",
+ new_mode: "100644",
+ old_mode: "100644",
+ old_hash: "def0123456789abcdef0123456789abcdef0123",
+ new_hash: "hij0123456789abcdef0123456789abcdef0123",
+ additions: 8,
+ deletions: 2,
+ },
+ {
+ path: "src/components/RangeSelector.js",
+ old_path: "src/components/RangePicker.js",
+ status: "R95",
+ new_mode: "100644",
+ old_mode: "100644",
+ old_hash: "cde0123456789abcdef0123456789abcdef0123",
+ new_hash: "klm0123456789abcdef0123456789abcdef0123",
+ additions: 5,
+ deletions: 3,
+ },
+ {
path: "src/styles/main.css",
old_path: "",
status: "M",
@@ -230,6 +252,110 @@
);
}`;
+ private dialogPickerJS = `function DialogPicker({ files, onFileSelect, onClose }) {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const [showDialog, setShowDialog] = useState(false);
+
+ // Similar to FilePicker but with modal dialog functionality
+ useEffect(() => {
+ setSelectedIndex(0);
+ if (files.length > 0) {
+ onFileSelect(files[0]);
+ }
+ }, [files, onFileSelect]);
+
+ return (
+ <div className="dialog-picker">
+ <button onClick={() => setShowDialog(true)}>
+ Open File Dialog
+ </button>
+ {showDialog && (
+ <div className="modal-overlay" onClick={() => setShowDialog(false)}>
+ <div className="modal-content" onClick={(e) => e.stopPropagation()}>
+ <h3>Select File</h3>
+ <select value={selectedIndex} onChange={(e) => {
+ const index = parseInt(e.target.value, 10);
+ setSelectedIndex(index);
+ onFileSelect(files[index]);
+ }}>
+ {files.map((file, index) => (
+ <option key={file.path} value={index}>
+ {file.status} {file.path}
+ </option>
+ ))}
+ </select>
+ <div className="modal-buttons">
+ <button onClick={() => setShowDialog(false)}>Close</button>
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}`;
+
+ private rangeSelectorJS = `function RangeSelector({ commits, onRangeChange, allowCustomRange = true }) {
+ const [rangeType, setRangeType] = useState('range');
+ const [startCommit, setStartCommit] = useState(commits[0]);
+ const [endCommit, setEndCommit] = useState(commits[commits.length - 1]);
+ const [customRange, setCustomRange] = useState('');
+
+ const handleTypeChange = (e) => {
+ setRangeType(e.target.value);
+ if (e.target.value === 'single') {
+ onRangeChange({ type: 'single', commit: startCommit });
+ } else if (e.target.value === 'custom' && customRange) {
+ onRangeChange({ type: 'custom', range: customRange });
+ } else {
+ onRangeChange({ type: 'range', from: startCommit, to: endCommit });
+ }
+ };
+
+ return (
+ <div className="range-selector">
+ <div className="range-type-selector">
+ <label>
+ <input
+ type="radio"
+ value="range"
+ checked={rangeType === 'range'}
+ onChange={handleTypeChange}
+ />
+ Commit Range
+ </label>
+ <label>
+ <input
+ type="radio"
+ value="single"
+ checked={rangeType === 'single'}
+ onChange={handleTypeChange}
+ />
+ Single Commit
+ </label>
+ {allowCustomRange && (
+ <label>
+ <input
+ type="radio"
+ value="custom"
+ checked={rangeType === 'custom'}
+ onChange={handleTypeChange}
+ />
+ Custom Range
+ </label>
+ )}
+ </div>
+ {rangeType === 'custom' && (
+ <input
+ type="text"
+ placeholder="Enter custom range (e.g., HEAD~5..HEAD)"
+ value={customRange}
+ onChange={(e) => setCustomRange(e.target.value)}
+ />
+ )}
+ </div>
+ );
+}`;
+
private mainCSSOriginal = `body {
font-family: sans-serif;
margin: 0;
@@ -353,6 +479,10 @@
return this.filePickerJS;
} else if (fileHash === "cde0123456789abcdef0123456789abcdef0123") {
return this.rangePickerJS;
+ } else if (fileHash === "hij0123456789abcdef0123456789abcdef0123") {
+ return this.dialogPickerJS;
+ } else if (fileHash === "klm0123456789abcdef0123456789abcdef0123") {
+ return this.rangeSelectorJS;
} else if (fileHash === "ghi0123456789abcdef0123456789abcdef0123") {
return this.mainCSSModified;
} else if (fileHash === "fgh0123456789abcdef0123456789abcdef0123") {
@@ -396,6 +526,10 @@
return this.filePickerJS;
} else if (filePath === "src/components/RangePicker.js") {
return this.rangePickerJS;
+ } else if (filePath === "src/components/DialogPicker.js") {
+ return this.dialogPickerJS;
+ } else if (filePath === "src/components/RangeSelector.js") {
+ return this.rangeSelectorJS;
} else if (filePath === "src/styles/main.css") {
return this.mainCSSModified;
}
diff --git a/webui/src/web-components/sketch-diff2-view.ts b/webui/src/web-components/sketch-diff2-view.ts
index 650826c..59f59df 100644
--- a/webui/src/web-components/sketch-diff2-view.ts
+++ b/webui/src/web-components/sketch-diff2-view.ts
@@ -371,6 +371,11 @@
color: #0c5460;
}
+ .file-status.copied {
+ background-color: #e2e3ff;
+ color: #383d41;
+ }
+
.file-changes {
margin-left: 8px;
font-size: 12px;
@@ -873,10 +878,14 @@
case "D":
return "deleted";
case "R":
+ case "C":
default:
if (status.toUpperCase().startsWith("R")) {
return "renamed";
}
+ if (status.toUpperCase().startsWith("C")) {
+ return "copied";
+ }
return "modified";
}
}
@@ -893,10 +902,14 @@
case "D":
return "Deleted";
case "R":
+ case "C":
default:
if (status.toUpperCase().startsWith("R")) {
return "Renamed";
}
+ if (status.toUpperCase().startsWith("C")) {
+ return "Copied";
+ }
return "Modified";
}
}