feat: add MergeDialog component and integrate with GitToolbar
- Create MergeDialog component for branch merging - Add merge button to GitToolbar - Show source branch selector and target branch display - Handle merge success and error states - Add modal CSS styles Completes Task 6.2 of git-control change.
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
pushRepository,
|
||||
type GitStatus,
|
||||
} from "../api/git_repositories";
|
||||
import { MergeDialog } from "./merge-dialog";
|
||||
|
||||
interface GitToolbarProps {
|
||||
projectId: string;
|
||||
@@ -33,6 +34,7 @@ export const GitToolbar = ({
|
||||
const [showNewBranch, setShowNewBranch] = useState(false);
|
||||
const [newBranchName, setNewBranchName] = useState("");
|
||||
const [newBranchBase, setNewBranchBase] = useState("");
|
||||
const [showMergeDialog, setShowMergeDialog] = useState(false);
|
||||
|
||||
const loadStatus = useCallback(async () => {
|
||||
try {
|
||||
@@ -179,6 +181,14 @@ export const GitToolbar = ({
|
||||
⇡ Push
|
||||
{status?.ahead ? <span className="badge">{status.ahead}</span> : null}
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-button"
|
||||
onClick={() => setShowMergeDialog(true)}
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
🔀 Merge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,6 +237,19 @@ export const GitToolbar = ({
|
||||
{status.untracked.length > 0 && <span className="status-badge untracked">❓ {status.untracked.length} untracked</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MergeDialog
|
||||
projectId={projectId}
|
||||
repoId={repoId}
|
||||
branches={branches}
|
||||
currentBranch={currentBranch}
|
||||
isOpen={showMergeDialog}
|
||||
onClose={() => setShowMergeDialog(false)}
|
||||
onMerge={() => {
|
||||
void loadStatus();
|
||||
onRefresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { mergeBranches } from "../api/git_repositories";
|
||||
|
||||
interface MergeDialogProps {
|
||||
projectId: string;
|
||||
repoId: string;
|
||||
branches: string[];
|
||||
currentBranch: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onMerge: () => void;
|
||||
}
|
||||
|
||||
export const MergeDialog = ({
|
||||
projectId,
|
||||
repoId,
|
||||
branches,
|
||||
currentBranch,
|
||||
isOpen,
|
||||
onClose,
|
||||
onMerge,
|
||||
}: MergeDialogProps) => {
|
||||
const [sourceBranch, setSourceBranch] = useState("");
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const availableBranches = branches.filter((b) => b !== currentBranch);
|
||||
|
||||
const handleMerge = async () => {
|
||||
if (!sourceBranch) {
|
||||
setError("Please select a source branch");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
try {
|
||||
await mergeBranches(
|
||||
projectId,
|
||||
repoId,
|
||||
sourceBranch,
|
||||
currentBranch,
|
||||
commitMessage || undefined
|
||||
);
|
||||
setSuccess(true);
|
||||
setTimeout(() => {
|
||||
onMerge();
|
||||
onClose();
|
||||
}, 1500);
|
||||
} catch {
|
||||
setError("Merge failed. There may be conflicts to resolve.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<h2>Merge Branch</h2>
|
||||
|
||||
<div className="merge-form">
|
||||
<div className="form-field">
|
||||
<label>Source Branch (merge from)</label>
|
||||
<select
|
||||
value={sourceBranch}
|
||||
onChange={(e) => setSourceBranch(e.target.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">Select branch...</option>
|
||||
{availableBranches.map((branch) => (
|
||||
<option key={branch} value={branch}>
|
||||
{branch}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label>Target Branch (merge into)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={currentBranch}
|
||||
disabled
|
||||
className="input-disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label>Commit Message (optional)</label>
|
||||
<textarea
|
||||
value={commitMessage}
|
||||
onChange={(e) => setCommitMessage(e.target.value)}
|
||||
placeholder={`Merge ${sourceBranch || "branch"} into ${currentBranch}`}
|
||||
rows={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="error-text">{error}</div>}
|
||||
{success && (
|
||||
<div className="success-text">Merge successful!</div>
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={handleMerge}
|
||||
disabled={loading || !sourceBranch}
|
||||
type="button"
|
||||
>
|
||||
{loading ? "Merging..." : "Merge"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -950,3 +950,87 @@ a {
|
||||
color: #ef4444;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Merge Dialog */
|
||||
.merge-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.merge-form .form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.merge-form label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.merge-form select,
|
||||
.merge-form input,
|
||||
.merge-form textarea {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.merge-form textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: #10b981;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user