mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-26 20:59:13 +01:00
762 lines
28 KiB
JavaScript
762 lines
28 KiB
JavaScript
import React, {useState, useEffect} from 'react';
|
|
import {
|
|
getSettings,
|
|
getGitStatus,
|
|
addFiles,
|
|
pushFiles,
|
|
revertFile,
|
|
pullBranch,
|
|
getDiff,
|
|
unlinkRepo
|
|
} from '../../api/api';
|
|
import UnlinkModal from './UnlinkModal';
|
|
import SettingsBranchModal from './SettingsBranchModal';
|
|
import {
|
|
FileText,
|
|
Code,
|
|
AlertCircle,
|
|
Plus,
|
|
MinusCircle,
|
|
Edit,
|
|
GitBranch,
|
|
Loader,
|
|
Eye,
|
|
RotateCcw,
|
|
Download,
|
|
ArrowDown,
|
|
ArrowUp,
|
|
CheckCircle,
|
|
File,
|
|
Settings,
|
|
Unlink
|
|
} from 'lucide-react';
|
|
import Alert from '../ui/Alert';
|
|
import CommitSection from './CommitSection';
|
|
import Tooltip from '../ui/Tooltip';
|
|
import DiffModal from './DiffModal';
|
|
import LinkRepoModal from './LinkRepoModal';
|
|
|
|
const SettingsPage = () => {
|
|
const [settings, setSettings] = useState(null);
|
|
const [status, setStatus] = useState(null);
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [showBranchModal, setShowBranchModal] = useState(false);
|
|
const [loadingAction, setLoadingAction] = useState('');
|
|
const [loadingStatus, setLoadingStatus] = useState(true);
|
|
const [loadingMessage, setLoadingMessage] = useState('');
|
|
const [commitMessage, setCommitMessage] = useState('');
|
|
const [showUnlinkModal, setShowUnlinkModal] = useState(false);
|
|
const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]);
|
|
const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]);
|
|
const [showDiffModal, setShowDiffModal] = useState(false);
|
|
const [diffContent, setDiffContent] = useState('');
|
|
const [currentChange, setCurrentChange] = useState(null);
|
|
const [loadingDiff, setLoadingDiff] = useState(false);
|
|
const [selectionType, setSelectionType] = useState(null);
|
|
const [funMessage, setFunMessage] = useState('');
|
|
const [showLinkModal, setShowLinkModal] = useState(false);
|
|
const [sortConfig, setSortConfig] = useState({
|
|
key: 'type',
|
|
direction: 'descending'
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchSettings();
|
|
}, []);
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
const fetchedSettings = await getSettings();
|
|
setSettings(fetchedSettings);
|
|
if (fetchedSettings) {
|
|
await fetchGitStatus();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching settings:', error);
|
|
}
|
|
};
|
|
|
|
const sortedChanges = changes => {
|
|
if (!sortConfig.key) return changes;
|
|
|
|
return [...changes].sort((a, b) => {
|
|
if (a[sortConfig.key] < b[sortConfig.key]) {
|
|
return sortConfig.direction === 'ascending' ? -1 : 1;
|
|
}
|
|
if (a[sortConfig.key] > b[sortConfig.key]) {
|
|
return sortConfig.direction === 'ascending' ? 1 : -1;
|
|
}
|
|
return 0;
|
|
});
|
|
};
|
|
|
|
const requestSort = key => {
|
|
let direction = 'ascending';
|
|
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
|
|
direction = 'descending';
|
|
}
|
|
setSortConfig({key, direction});
|
|
};
|
|
|
|
const SortableHeader = ({children, sortKey}) => {
|
|
const isSorted = sortConfig.key === sortKey;
|
|
return (
|
|
<th
|
|
className='px-4 py-2 text-left text-gray-300 cursor-pointer hover:bg-gray-500'
|
|
onClick={() => requestSort(sortKey)}>
|
|
<div className='flex items-center'>
|
|
{children}
|
|
{isSorted &&
|
|
(sortConfig.direction === 'ascending' ? (
|
|
<ArrowUp size={14} className='ml-1' />
|
|
) : (
|
|
<ArrowDown size={14} className='ml-1' />
|
|
))}
|
|
</div>
|
|
</th>
|
|
);
|
|
};
|
|
|
|
const fetchGitStatus = async () => {
|
|
setLoadingStatus(true);
|
|
setLoadingMessage(getRandomLoadingMessage());
|
|
setFunMessage(getRandomFunMessage());
|
|
try {
|
|
const result = await getGitStatus();
|
|
console.log('================ Git Status Response ================');
|
|
console.log(JSON.stringify(result, null, 2));
|
|
console.log('======================================================');
|
|
|
|
if (result.success) {
|
|
setStatus({
|
|
...result.data,
|
|
outgoing_changes: Array.isArray(result.data.outgoing_changes)
|
|
? result.data.outgoing_changes
|
|
: [],
|
|
incoming_changes: Array.isArray(result.data.incoming_changes)
|
|
? result.data.incoming_changes
|
|
: []
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching Git status:', error);
|
|
Alert.error('Failed to fetch Git status');
|
|
} finally {
|
|
setLoadingStatus(false);
|
|
}
|
|
};
|
|
|
|
const getRandomFunMessage = () => {
|
|
const funMessages = [
|
|
'No changes detected. Your regex is so precise, it could find a needle in a haystack... made of needles. 🧵🔍',
|
|
'All quiet on the commit front. Your custom formats are so perfect, even perfectionists are jealous. 🏆',
|
|
"No updates needed. Your media automation is running so smoothly, it's making butter jealous. 🧈",
|
|
'Zero modifications. Your torrent setup is seeding so efficiently, farmers are asking for advice. 🌾',
|
|
"No edits required. Your regex fu is so strong, it's bench-pressing parentheses for fun. 💪()",
|
|
'Unchanged status. Your Plex library is so well-organized, librarians are taking notes. 📚🤓',
|
|
"No alterations found. Your file naming scheme is so consistent, it's bringing tears to OCD eyes. 😢👀",
|
|
"All systems nominal. Your download queue is so orderly, it's making Marie Kondo question her career. 🧹✨",
|
|
"No revisions necessary. Your automation scripts are so smart, they're solving captchas for fun. 🤖🧩",
|
|
'Steady as she goes. Your media collection is so complete, Netflix is asking you for recommendations. 🎬👑'
|
|
];
|
|
return funMessages[Math.floor(Math.random() * funMessages.length)];
|
|
};
|
|
|
|
const renderChangeTable = (changes, title, icon, isIncoming) => (
|
|
<div className='mb-4'>
|
|
<h4 className='text-sm font-medium text-gray-200 mb-2 flex items-center'>
|
|
{icon}
|
|
<span>
|
|
{title} ({changes.length})
|
|
</span>
|
|
</h4>
|
|
<div className='border border-gray-600 rounded-md overflow-hidden'>
|
|
<table className='w-full text-sm'>
|
|
<thead className='bg-gray-600'>
|
|
<tr>
|
|
<SortableHeader sortKey='status'>Status</SortableHeader>
|
|
<SortableHeader sortKey='type'>Type</SortableHeader>
|
|
<SortableHeader sortKey='name'>Name</SortableHeader>
|
|
<th className='px-4 py-2 text-left text-gray-300 w-1/5'>
|
|
Actions
|
|
</th>
|
|
<th className='px-4 py-2 text-right text-gray-300 w-1/10'>
|
|
Select
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sortedChanges(changes).map((change, index) => (
|
|
<tr
|
|
key={`${isIncoming ? 'incoming' : 'outgoing'}-${index}`}
|
|
className={`border-t border-gray-600 cursor-pointer hover:bg-gray-700 ${
|
|
(isIncoming
|
|
? selectedIncomingChanges
|
|
: selectedOutgoingChanges
|
|
).includes(change.file_path)
|
|
? 'bg-gray-700'
|
|
: ''
|
|
}`}
|
|
onClick={() =>
|
|
handleSelectChange(change.file_path, isIncoming)
|
|
}>
|
|
<td className='px-4 py-2 text-gray-300'>
|
|
<div className='flex items-center'>
|
|
{getStatusIcon(change.status)}
|
|
<span className='ml-2'>
|
|
{change.staged
|
|
? `${change.status} (Staged)`
|
|
: change.status}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className='px-4 py-2 text-gray-300'>
|
|
<div className='flex items-center'>
|
|
{getTypeIcon(change.type)}
|
|
<span className='ml-2'>{change.type}</span>
|
|
</div>
|
|
</td>
|
|
<td className='px-4 py-2 text-gray-300'>
|
|
{change.name || 'Unnamed'}
|
|
</td>
|
|
<td className='px-4 py-2 text-left align-middle'>
|
|
<Tooltip content='View differences'>
|
|
<button
|
|
onClick={e => {
|
|
e.stopPropagation();
|
|
handleViewDiff(change);
|
|
}}
|
|
className='flex items-center justify-center px-2 py-1 bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors text-xs'
|
|
style={{width: '100%'}}>
|
|
{loadingDiff ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<>
|
|
<Eye size={12} className='mr-1' />
|
|
View Diff
|
|
</>
|
|
)}
|
|
</button>
|
|
</Tooltip>
|
|
</td>
|
|
<td className='px-4 py-2 text-right text-gray-300 align-middle'>
|
|
<input
|
|
type='checkbox'
|
|
checked={(isIncoming
|
|
? selectedIncomingChanges
|
|
: selectedOutgoingChanges
|
|
).includes(change.file_path)}
|
|
onChange={e => e.stopPropagation()}
|
|
disabled={!isIncoming && change.staged}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const getStageButtonTooltip = () => {
|
|
if (selectionType === 'staged') {
|
|
return 'These files are already staged';
|
|
}
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
return 'Select files to stage';
|
|
}
|
|
return 'Stage selected files';
|
|
};
|
|
|
|
const getCommitButtonTooltip = () => {
|
|
if (selectionType === 'unstaged') {
|
|
return 'You can only commit staged files';
|
|
}
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
return 'Select files to commit';
|
|
}
|
|
if (!commitMessage.trim()) {
|
|
return 'Enter a commit message';
|
|
}
|
|
return 'Commit selected files';
|
|
};
|
|
|
|
const getRevertButtonTooltip = () => {
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
return 'Select files to revert';
|
|
}
|
|
return 'Revert selected files';
|
|
};
|
|
|
|
const handleViewDiff = async change => {
|
|
setLoadingDiff(true);
|
|
try {
|
|
const response = await getDiff(change.file_path);
|
|
console.log('Diff response:', response); // Add this line to log the response
|
|
if (response.success) {
|
|
console.log('Diff content:', response.diff); // Add this line to log the diff content
|
|
setDiffContent(response.diff);
|
|
setCurrentChange(change);
|
|
setShowDiffModal(true);
|
|
} else {
|
|
Alert.error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Alert.error('An unexpected error occurred while fetching the diff.');
|
|
console.error('Error fetching diff:', error);
|
|
} finally {
|
|
setLoadingDiff(false);
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
const handleStageSelectedChanges = async () => {
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
Alert.warning('Please select at least one change to stage.');
|
|
return;
|
|
}
|
|
|
|
setLoadingAction('stage_selected');
|
|
try {
|
|
const response = await addFiles(selectedOutgoingChanges);
|
|
if (response.success) {
|
|
await fetchGitStatus();
|
|
setSelectedOutgoingChanges([]); // Clear the selected changes after staging
|
|
Alert.success(response.message);
|
|
} else {
|
|
Alert.error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Alert.error('An unexpected error occurred while staging changes.');
|
|
console.error('Error staging changes:', error);
|
|
} finally {
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
const handleCommitSelectedChanges = async () => {
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
Alert.warning('Please select at least one change to commit.');
|
|
return;
|
|
}
|
|
|
|
if (!commitMessage.trim()) {
|
|
Alert.warning('Please enter a commit message.');
|
|
return;
|
|
}
|
|
|
|
setLoadingAction('commit_selected');
|
|
try {
|
|
const response = await pushFiles(selectedOutgoingChanges, commitMessage);
|
|
if (response.success) {
|
|
await fetchGitStatus();
|
|
setSelectedOutgoingChanges([]); // Clear the selected changes after committing
|
|
setCommitMessage('');
|
|
Alert.success(response.message);
|
|
} else {
|
|
Alert.error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Alert.error('An unexpected error occurred while committing changes.');
|
|
console.error('Error committing changes:', error);
|
|
} finally {
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
const handleRevertSelectedChanges = async () => {
|
|
if (selectedOutgoingChanges.length === 0) {
|
|
Alert.warning('Please select at least one change to revert.');
|
|
return;
|
|
}
|
|
|
|
setLoadingAction('revert_selected');
|
|
try {
|
|
const response = await Promise.all(
|
|
selectedOutgoingChanges.map(filePath => revertFile(filePath))
|
|
);
|
|
const allSuccessful = response.every(res => res.success);
|
|
if (allSuccessful) {
|
|
await fetchGitStatus();
|
|
setSelectedOutgoingChanges([]); // Clear the selected changes after reverting
|
|
Alert.success('Selected changes have been reverted successfully.');
|
|
} else {
|
|
Alert.error('Some changes could not be reverted. Please try again.');
|
|
}
|
|
} catch (error) {
|
|
Alert.error('An unexpected error occurred while reverting changes.');
|
|
console.error('Error reverting changes:', error);
|
|
} finally {
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
const handlePullSelectedChanges = async () => {
|
|
if (selectedIncomingChanges.length === 0) {
|
|
Alert.warning('Please select at least one change to pull.');
|
|
return;
|
|
}
|
|
|
|
setLoadingAction('pull_changes');
|
|
try {
|
|
// You would need to update your backend to handle pulling specific files
|
|
const response = await pullBranch(status.branch, selectedIncomingChanges);
|
|
if (response.success) {
|
|
await fetchGitStatus();
|
|
setSelectedIncomingChanges([]); // Clear the selected changes after pulling
|
|
Alert.success(response.message);
|
|
} else {
|
|
Alert.error(response.error);
|
|
}
|
|
} catch (error) {
|
|
Alert.error('An unexpected error occurred while pulling changes.');
|
|
console.error('Error pulling changes:', error);
|
|
} finally {
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
const handleSelectChange = (filePath, isIncoming) => {
|
|
if (isIncoming) {
|
|
setSelectedIncomingChanges(prevSelected => {
|
|
if (prevSelected.includes(filePath)) {
|
|
return prevSelected.filter(path => path !== filePath);
|
|
} else {
|
|
return [...prevSelected, filePath];
|
|
}
|
|
});
|
|
} else {
|
|
const change = status.outgoing_changes.find(
|
|
c => c.file_path === filePath
|
|
);
|
|
const isStaged = change.staged;
|
|
|
|
setSelectedOutgoingChanges(prevSelected => {
|
|
if (prevSelected.includes(filePath)) {
|
|
const newSelection = prevSelected.filter(path => path !== filePath);
|
|
if (newSelection.length === 0) setSelectionType(null);
|
|
return newSelection;
|
|
} else {
|
|
if (
|
|
prevSelected.length === 0 ||
|
|
(isStaged && selectionType === 'staged') ||
|
|
(!isStaged && selectionType === 'unstaged')
|
|
) {
|
|
setSelectionType(isStaged ? 'staged' : 'unstaged');
|
|
return [...prevSelected, filePath];
|
|
} else {
|
|
return prevSelected;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const loadingMessages = [
|
|
"Checking for changes... don't blink!",
|
|
'Syncing with the mothership...',
|
|
'Peeking under the hood...',
|
|
'Counting bits and bytes...',
|
|
'Scanning for modifications...',
|
|
'Looking for new stuff...',
|
|
'Comparing local and remote...',
|
|
"Checking your project's pulse...",
|
|
"Analyzing your code's mood...",
|
|
"Reading the project's diary..."
|
|
];
|
|
|
|
const getRandomLoadingMessage = () => {
|
|
return loadingMessages[Math.floor(Math.random() * loadingMessages.length)];
|
|
};
|
|
|
|
const getStatusIcon = status => {
|
|
switch (status) {
|
|
case 'Untracked':
|
|
return <Plus className='text-blue-400' size={16} />;
|
|
case 'Staged (New)':
|
|
return <Plus className='text-green-400' size={16} />;
|
|
case 'Staged (Modified)':
|
|
case 'Modified':
|
|
return <Edit className='text-yellow-400' size={16} />;
|
|
case 'Deleted':
|
|
return <MinusCircle className='text-red-400' size={16} />;
|
|
case 'Deleted (Staged)':
|
|
return <MinusCircle className='text-red-600' size={16} />;
|
|
case 'Renamed':
|
|
return <GitBranch className='text-purple-400' size={16} />;
|
|
default:
|
|
return <AlertCircle className='text-gray-400' size={16} />;
|
|
}
|
|
};
|
|
|
|
const getTypeIcon = type => {
|
|
switch (type) {
|
|
case 'Regex Pattern':
|
|
return <Code className='text-blue-400' size={16} />;
|
|
case 'Custom Format':
|
|
return <FileText className='text-green-400' size={16} />;
|
|
case 'Quality Profile':
|
|
return <Settings className='text-purple-400' size={16} />;
|
|
default:
|
|
return <File className='text-gray-400' size={16} />;
|
|
}
|
|
};
|
|
|
|
const handleLinkRepo = async () => {
|
|
setLoadingAction('');
|
|
setShowLinkModal(false);
|
|
await fetchSettings();
|
|
};
|
|
|
|
const handleUnlinkRepo = async removeFiles => {
|
|
setLoadingAction('unlink_repo');
|
|
try {
|
|
const response = await unlinkRepo(removeFiles);
|
|
if (response.success) {
|
|
setSettings(null);
|
|
setStatus(null);
|
|
Alert.success('Repository unlinked successfully');
|
|
setShowUnlinkModal(false); // Close the modal after unlinking
|
|
} else {
|
|
Alert.error(response.error || 'Failed to unlink repository');
|
|
}
|
|
} catch (error) {
|
|
Alert.error(
|
|
'An unexpected error occurred while unlinking the repository'
|
|
);
|
|
console.error('Error unlinking repository:', error);
|
|
} finally {
|
|
setLoadingAction('');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className='max-w-4xl mx-auto mt-8 p-6 bg-gray-800 rounded-lg shadow-lg'>
|
|
<h2 className='text-xl font-bold mb-4 text-gray-100'>
|
|
Git Repository Settings
|
|
</h2>
|
|
{!settings && (
|
|
<button
|
|
onClick={() => setShowLinkModal(true)}
|
|
className='flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'>
|
|
Link Repository
|
|
</button>
|
|
)}
|
|
{settings && (
|
|
<div className='space-y-4'>
|
|
<div className='bg-gray-700 p-4 rounded-md'>
|
|
<h3 className='text-sm font-semibold text-gray-100 mb-2'>
|
|
Connected Repository
|
|
</h3>
|
|
<div className='flex items-center justify-between'>
|
|
<a
|
|
href={settings.gitRepo}
|
|
target='_blank'
|
|
rel='noopener noreferrer'
|
|
className='text-blue-400 hover:text-blue-300 transition-colors text-sm'>
|
|
{settings.gitRepo}
|
|
</a>
|
|
<div className='flex space-x-2'>
|
|
<Tooltip content='Unlink Repository'>
|
|
<button
|
|
onClick={() => setShowUnlinkModal(true)}
|
|
className='flex items-center px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'unlink_repo'}>
|
|
{loadingAction === 'unlink_repo' ? (
|
|
<Loader size={14} className='animate-spin' />
|
|
) : (
|
|
<Unlink size={14} className='mr-2' />
|
|
)}
|
|
Unlink
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className='bg-gray-700 p-4 rounded-md'>
|
|
<h3 className='text-sm font-semibold text-gray-100 mb-2'>
|
|
Git Status
|
|
</h3>
|
|
{loadingStatus ? (
|
|
<div className='flex items-center justify-center'>
|
|
<Loader size={24} className='animate-spin text-gray-300' />
|
|
<span className='ml-2 text-gray-300 text-sm'>
|
|
{loadingMessage}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
status && (
|
|
<>
|
|
<div className='flex items-center justify-between mb-4'>
|
|
<div className='flex items-center'>
|
|
<GitBranch className='mr-2 text-green-400' size={14} />
|
|
<span className='text-gray-200 text-sm'>
|
|
Current Branch: {status.branch}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowBranchModal(true)}
|
|
className='flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'>
|
|
<Eye size={14} className='mr-2' />
|
|
View Branches
|
|
</button>
|
|
</div>
|
|
|
|
{status.incoming_changes.length > 0 &&
|
|
renderChangeTable(
|
|
status.incoming_changes,
|
|
'Incoming Changes',
|
|
<ArrowDown className='text-yellow-400 mr-2' size={16} />,
|
|
true
|
|
)}
|
|
{status.outgoing_changes.length > 0 &&
|
|
renderChangeTable(
|
|
status.outgoing_changes,
|
|
'Outgoing Changes',
|
|
<ArrowUp className='text-blue-400 mr-2' size={16} />,
|
|
false
|
|
)}
|
|
|
|
<CommitSection
|
|
status={status}
|
|
commitMessage={commitMessage}
|
|
setCommitMessage={setCommitMessage}
|
|
loadingAction={loadingAction}
|
|
hasIncomingChanges={status.incoming_changes.length > 0}
|
|
funMessage={funMessage}
|
|
/>
|
|
|
|
{/* Buttons Below Commit Section */}
|
|
<div className='mt-4 flex justify-end space-x-2'>
|
|
{/* Conditionally render Stage button */}
|
|
{selectedOutgoingChanges.length > 0 &&
|
|
selectionType !== 'staged' && (
|
|
<Tooltip content={getStageButtonTooltip()}>
|
|
<button
|
|
onClick={handleStageSelectedChanges}
|
|
className='flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'stage_selected'}>
|
|
{loadingAction === 'stage_selected' ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<Plus className='mr-1' size={12} />
|
|
)}
|
|
Stage Selected
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
<div className='mt-4 flex justify-end space-x-2'>
|
|
{/* Stage button */}
|
|
{selectedOutgoingChanges.length > 0 &&
|
|
selectionType !== 'staged' && (
|
|
<Tooltip content={getStageButtonTooltip()}>
|
|
<button
|
|
onClick={handleStageSelectedChanges}
|
|
className='flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'stage_selected'}>
|
|
{loadingAction === 'stage_selected' ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<Plus className='mr-1' size={12} />
|
|
)}
|
|
Stage Selected
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{/* Commit button */}
|
|
{selectedOutgoingChanges.length > 0 &&
|
|
commitMessage.trim() &&
|
|
selectionType !== 'unstaged' && (
|
|
<Tooltip content={getCommitButtonTooltip()}>
|
|
<button
|
|
onClick={handleCommitSelectedChanges}
|
|
className='flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'commit_selected'}>
|
|
{loadingAction === 'commit_selected' ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<CheckCircle className='mr-1' size={12} />
|
|
)}
|
|
Commit Selected
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
|
|
{/* Revert button */}
|
|
{selectedOutgoingChanges.length > 0 && (
|
|
<Tooltip content={getRevertButtonTooltip()}>
|
|
<button
|
|
onClick={handleRevertSelectedChanges}
|
|
className='flex items-center px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'revert_selected'}>
|
|
{loadingAction === 'revert_selected' ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<RotateCcw className='mr-1' size={12} />
|
|
)}
|
|
Revert Selected
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pull button (always enabled) */}
|
|
{selectedIncomingChanges.length > 0 && (
|
|
<Tooltip content='Pull selected changes'>
|
|
<button
|
|
onClick={handlePullSelectedChanges}
|
|
className='flex items-center px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 transition-colors duration-200 ease-in-out text-xs'
|
|
disabled={loadingAction === 'pull_changes'}>
|
|
{loadingAction === 'pull_changes' ? (
|
|
<Loader size={12} className='animate-spin' />
|
|
) : (
|
|
<Download className='mr-1' size={12} />
|
|
)}
|
|
Pull Selected
|
|
</button>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{settings && status && (
|
|
<SettingsBranchModal
|
|
isOpen={showBranchModal}
|
|
onClose={() => setShowBranchModal(false)}
|
|
repoUrl={settings.gitRepo}
|
|
currentBranch={status.branch}
|
|
onBranchChange={fetchGitStatus}
|
|
/>
|
|
)}
|
|
{showDiffModal && currentChange && (
|
|
<DiffModal
|
|
isOpen={showDiffModal}
|
|
onClose={() => setShowDiffModal(false)}
|
|
diffContent={diffContent}
|
|
type={currentChange.type}
|
|
name={currentChange.name}
|
|
commitMessage={currentChange.commit_message}
|
|
/>
|
|
)}
|
|
<LinkRepoModal
|
|
isOpen={showLinkModal}
|
|
onClose={() => setShowLinkModal(false)}
|
|
onSubmit={handleLinkRepo}
|
|
/>
|
|
<UnlinkModal
|
|
isOpen={showUnlinkModal}
|
|
onClose={() => setShowUnlinkModal(false)}
|
|
onSubmit={handleUnlinkRepo}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SettingsPage;
|