diff --git a/apps/web/src/components/git-toolbar.tsx b/apps/web/src/components/git-toolbar.tsx index 4c92c59..37cdea2 100644 --- a/apps/web/src/components/git-toolbar.tsx +++ b/apps/web/src/components/git-toolbar.tsx @@ -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 ? {status.ahead} : null} + setShowMergeDialog(true)} + disabled={loading} + type="button" + > + 🔀 Merge + @@ -227,6 +237,19 @@ export const GitToolbar = ({ {status.untracked.length > 0 && ❓ {status.untracked.length} untracked} )} + + setShowMergeDialog(false)} + onMerge={() => { + void loadStatus(); + onRefresh(); + }} + /> ); }; diff --git a/apps/web/src/components/merge-dialog.tsx b/apps/web/src/components/merge-dialog.tsx new file mode 100644 index 0000000..1bb6c28 --- /dev/null +++ b/apps/web/src/components/merge-dialog.tsx @@ -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(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 ( + + e.stopPropagation()}> + Merge Branch + + + + Source Branch (merge from) + setSourceBranch(e.target.value)} + disabled={loading} + > + Select branch... + {availableBranches.map((branch) => ( + + {branch} + + ))} + + + + + Target Branch (merge into) + + + + + Commit Message (optional) + setCommitMessage(e.target.value)} + placeholder={`Merge ${sourceBranch || "branch"} into ${currentBranch}`} + rows={3} + disabled={loading} + /> + + + {error && {error}} + {success && ( + Merge successful! + )} + + + + Cancel + + + {loading ? "Merging..." : "Merge"} + + + + + + ); +}; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 9cf4656..93bff71 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -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; +}