mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-27 05:00:53 +01:00
refactor: complete refactor for settings page
This commit is contained in:
@@ -38,12 +38,6 @@ const CommitSection = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{hasUnstagedChanges && !hasStagedChanges && (
|
||||
<p className='text-yellow-400 text-sm mb-2'>
|
||||
You have unstaged changes. Stage your
|
||||
changes before committing.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className='text-gray-300 text-sm italic'>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Modal from "../ui/Modal";
|
||||
|
||||
const DiffModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
diffContent,
|
||||
type,
|
||||
name,
|
||||
commitMessage,
|
||||
title = "View Diff",
|
||||
}) => {
|
||||
const formatDiffContent = (content) => {
|
||||
if (!content) return [];
|
||||
return content.split("\n").map((line, index) => {
|
||||
let lineClass = "py-1 pl-4 border-l-2 ";
|
||||
if (line.startsWith("+")) {
|
||||
lineClass += "bg-green-900/30 text-green-400 border-green-500";
|
||||
} else if (line.startsWith("-")) {
|
||||
lineClass += "bg-red-900/30 text-red-400 border-red-500";
|
||||
} else {
|
||||
lineClass += "border-transparent";
|
||||
}
|
||||
return (
|
||||
<div key={index} className={`flex ${lineClass}`}>
|
||||
<span className="w-12 text-gray-500 select-none text-right pr-4 border-r border-gray-700">
|
||||
{index + 1}
|
||||
</span>
|
||||
<code className="flex-1 pl-4 font-mono text-sm">{line}</code>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const formattedContent = formatDiffContent(diffContent);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={title} size="4xl">
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-300">
|
||||
Type:
|
||||
</span>
|
||||
<span className="bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-300">
|
||||
Name:
|
||||
</span>
|
||||
<span className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 px-2 py-1 rounded">
|
||||
{name === "Deleted File" ? "Deleted File" : name}
|
||||
</span>
|
||||
</div>
|
||||
{commitMessage && (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-300 mb-1">
|
||||
Commit Message:
|
||||
</span>
|
||||
<p className="bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 p-2 rounded">
|
||||
{commitMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-2 text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
|
||||
Diff Content
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-900 p-4 max-h-[60vh] overflow-y-auto">
|
||||
{formattedContent.length > 0 ? (
|
||||
formattedContent
|
||||
) : (
|
||||
<div className="text-gray-500 dark:text-gray-400 italic">
|
||||
No differences found or file is empty.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
DiffModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
diffContent: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
commitMessage: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
export default DiffModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
getSettings,
|
||||
getGitStatus,
|
||||
@@ -6,58 +6,27 @@ import {
|
||||
pushFiles,
|
||||
revertFile,
|
||||
pullBranch,
|
||||
getDiff,
|
||||
unlinkRepo,
|
||||
checkDevMode
|
||||
} from '../../api/api';
|
||||
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 SettingsBranchModal from './SettingsBranchModal';
|
||||
import Alert from '../ui/Alert';
|
||||
import CommitSection from './CommitSection';
|
||||
import Tooltip from '../ui/Tooltip';
|
||||
import DiffModal from './DiffModal';
|
||||
import ArrContainer from './arrs/ArrContainer';
|
||||
import RepoContainer from './git/RepoContainer';
|
||||
import StatusContainer from './git/StatusContainer';
|
||||
import { statusLoadingMessages, noChangesMessages, getRandomMessage } from '../../utils/messages';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const [settings, setSettings] = useState(null);
|
||||
const [status, setStatus] = useState(null);
|
||||
const [changes, setChanges] = useState(null);
|
||||
const [isDevMode, setIsDevMode] = 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 [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 [sortConfig, setSortConfig] = useState({
|
||||
key: 'type',
|
||||
direction: 'descending'
|
||||
});
|
||||
const [statusLoading, setStatusLoading] = useState(true);
|
||||
const [statusLoadingMessage, setStatusLoadingMessage] = useState('');
|
||||
const [noChangesMessage, setNoChangesMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -86,72 +55,19 @@ const SettingsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
setStatusLoading(true);
|
||||
setStatusLoadingMessage(getRandomMessage(statusLoadingMessages));
|
||||
setNoChangesMessage(getRandomMessage(noChangesMessages));
|
||||
try {
|
||||
const result = await getGitStatus();
|
||||
console.log(
|
||||
'================ Git Status Response ================'
|
||||
);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
console.log(
|
||||
'======================================================'
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
setStatus({
|
||||
setChanges({
|
||||
...result.data,
|
||||
outgoing_changes: Array.isArray(
|
||||
result.data.outgoing_changes
|
||||
)
|
||||
outgoing_changes: Array.isArray(result.data.outgoing_changes)
|
||||
? result.data.outgoing_changes
|
||||
: [],
|
||||
incoming_changes: Array.isArray(
|
||||
result.data.incoming_changes
|
||||
)
|
||||
incoming_changes: Array.isArray(result.data.incoming_changes)
|
||||
? result.data.incoming_changes
|
||||
: []
|
||||
});
|
||||
@@ -160,206 +76,16 @@ const SettingsPage = () => {
|
||||
console.error('Error fetching Git status:', error);
|
||||
Alert.error('Failed to fetch Git status');
|
||||
} finally {
|
||||
setLoadingStatus(false);
|
||||
setStatusLoading(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>
|
||||
{isIncoming
|
||||
? title
|
||||
: isDevMode
|
||||
? 'Outgoing Changes'
|
||||
: 'Local Changes'}{' '}
|
||||
({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;
|
||||
}
|
||||
|
||||
const handleStageSelectedChanges = async (selectedChanges) => {
|
||||
setLoadingAction('stage_selected');
|
||||
try {
|
||||
const response = await addFiles(selectedOutgoingChanges);
|
||||
const response = await addFiles(selectedChanges);
|
||||
if (response.success) {
|
||||
await fetchGitStatus();
|
||||
setSelectedOutgoingChanges([]); // Clear the selected changes after staging
|
||||
Alert.success(response.message);
|
||||
} else {
|
||||
Alert.error(response.error);
|
||||
@@ -372,90 +98,49 @@ const SettingsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const handleCommitSelectedChanges = async (selectedChanges, commitMessage) => {
|
||||
setLoadingAction('commit_selected');
|
||||
try {
|
||||
const response = await pushFiles(
|
||||
selectedOutgoingChanges,
|
||||
commitMessage
|
||||
);
|
||||
const response = await pushFiles(selectedChanges, 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.'
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
const handleRevertSelectedChanges = async (selectedChanges) => {
|
||||
setLoadingAction('revert_selected');
|
||||
try {
|
||||
const response = await Promise.all(
|
||||
selectedOutgoingChanges.map(filePath => revertFile(filePath))
|
||||
);
|
||||
const response = await Promise.all(selectedChanges.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.'
|
||||
);
|
||||
Alert.success('Selected changes have been reverted successfully.');
|
||||
} else {
|
||||
Alert.error(
|
||||
'Some changes could not be reverted. Please try again.'
|
||||
);
|
||||
Alert.error('Some changes could not be reverted. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.error(
|
||||
'An unexpected error occurred while reverting changes.'
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
const handlePullSelectedChanges = async (selectedChanges) => {
|
||||
setLoadingAction('pull_changes');
|
||||
try {
|
||||
// You would need to update your backend to handle pulling specific files
|
||||
const response = await pullBranch(
|
||||
status.branch,
|
||||
selectedIncomingChanges
|
||||
);
|
||||
const response = await pullBranch(changes.branch, selectedChanges);
|
||||
if (response.success) {
|
||||
await fetchGitStatus();
|
||||
setSelectedIncomingChanges([]); // Clear the selected changes after pulling
|
||||
Alert.success(response.message);
|
||||
} else {
|
||||
Alert.error(response.error);
|
||||
@@ -468,101 +153,11 @@ const SettingsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className='text-xl font-bold mb-4 text-gray-100'>
|
||||
Git Repository Settings
|
||||
<span className='ml-4 text-sm text-gray-300 mb-4'>
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-100">
|
||||
Git Settings
|
||||
<span className="ml-4 text-sm text-gray-300 mb-4">
|
||||
{isDevMode ? 'Dev Mode: Enabled' : 'Dev Mode: Disabled'}
|
||||
</span>
|
||||
</h2>
|
||||
@@ -574,248 +169,48 @@ const SettingsPage = () => {
|
||||
/>
|
||||
|
||||
{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'>
|
||||
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}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
{/* Buttons Below Commit Section */}
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
{isDevMode && (
|
||||
<>
|
||||
{/* 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 (moved outside isDevMode check) */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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 className="space-y-4">
|
||||
{statusLoading ? (
|
||||
<div className="flex items-left justify-left dark:bg-gray-800 p-4 rounded-md border border-gray-200 dark:border-gray-700 text-sm">
|
||||
<Loader className="animate-spin mr-2" size={16} />
|
||||
<span className="text-gray-300">{statusLoadingMessage}</span>
|
||||
</div>
|
||||
) : changes && (changes.incoming_changes.length > 0 || changes.outgoing_changes.length > 0) ? (
|
||||
<StatusContainer
|
||||
status={changes}
|
||||
isDevMode={isDevMode}
|
||||
onViewBranches={() => setShowBranchModal(true)}
|
||||
onStageSelected={handleStageSelectedChanges}
|
||||
onCommitSelected={handleCommitSelectedChanges}
|
||||
onRevertSelected={handleRevertSelectedChanges}
|
||||
onPullSelected={handlePullSelectedChanges}
|
||||
loadingAction={loadingAction}
|
||||
/>
|
||||
) : (
|
||||
<div className="dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded-md text-gray-300 text-left text-sm">
|
||||
{noChangesMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<h2 className='text-xl font-bold mb-4 text-gray-100 mt-3'>
|
||||
|
||||
<h2 className="text-xl font-bold mb-4 text-gray-100 mt-3">
|
||||
Arr Management
|
||||
</h2>
|
||||
<ArrContainer />
|
||||
{settings && status && (
|
||||
|
||||
{settings && changes && (
|
||||
<SettingsBranchModal
|
||||
isOpen={showBranchModal}
|
||||
onClose={() => setShowBranchModal(false)}
|
||||
repoUrl={settings.gitRepo}
|
||||
currentBranch={status.branch}
|
||||
currentBranch={changes.branch}
|
||||
onBranchChange={fetchGitStatus}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
{showDiffModal && currentChange && (
|
||||
<DiffModal
|
||||
isOpen={showDiffModal}
|
||||
onClose={() => setShowDiffModal(false)}
|
||||
diffContent={diffContent}
|
||||
type={currentChange.type}
|
||||
name={currentChange.name}
|
||||
commitMessage={currentChange.commit_message}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
export default SettingsPage;
|
||||
148
frontend/src/components/settings/git/ChangeRow.jsx
Normal file
148
frontend/src/components/settings/git/ChangeRow.jsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
Eye,
|
||||
Loader,
|
||||
Plus,
|
||||
MinusCircle,
|
||||
Edit,
|
||||
GitBranch,
|
||||
AlertCircle,
|
||||
Code,
|
||||
FileText,
|
||||
Settings,
|
||||
File
|
||||
} from 'lucide-react';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import {getDiff} from '../../../api/api';
|
||||
import Alert from '../../ui/Alert';
|
||||
import Diff from './modal/ViewDiff';
|
||||
|
||||
const ChangeRow = ({change, isSelected, onSelect, isIncoming, isDevMode}) => {
|
||||
const [loadingDiff, setLoadingDiff] = useState(false);
|
||||
const [showDiff, setShowDiff] = useState(false);
|
||||
const [diffContent, setDiffContent] = useState('');
|
||||
|
||||
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 handleViewDiff = async () => {
|
||||
setLoadingDiff(true);
|
||||
try {
|
||||
const response = await getDiff(change.file_path);
|
||||
if (response.success) {
|
||||
setDiffContent(response.diff);
|
||||
setShowDiff(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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
className={`border-t border-gray-600 cursor-pointer hover:bg-gray-700 ${
|
||||
isSelected ? 'bg-gray-700' : ''
|
||||
}`}
|
||||
onClick={() => onSelect(change.file_path)}>
|
||||
<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();
|
||||
}}
|
||||
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={isSelected}
|
||||
onChange={e => e.stopPropagation()}
|
||||
disabled={!isIncoming && change.staged}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{showDiff && (
|
||||
<Diff
|
||||
isOpen={showDiff}
|
||||
onClose={() => setShowDiff(false)}
|
||||
diffContent={diffContent}
|
||||
type={change.type}
|
||||
name={change.name}
|
||||
commitMessage={change.commit_message}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeRow;
|
||||
103
frontend/src/components/settings/git/ChangeTable.jsx
Normal file
103
frontend/src/components/settings/git/ChangeTable.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import {ArrowDown, ArrowUp} from 'lucide-react';
|
||||
import ChangeRow from './ChangeRow';
|
||||
|
||||
const ChangeTable = ({
|
||||
changes,
|
||||
title,
|
||||
icon,
|
||||
isIncoming,
|
||||
selectedChanges,
|
||||
onSelectChange,
|
||||
sortConfig,
|
||||
onRequestSort,
|
||||
isDevMode
|
||||
}) => {
|
||||
const sortedChanges = changesArray => {
|
||||
if (!sortConfig.key) return changesArray;
|
||||
|
||||
return [...changesArray].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 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={() => onRequestSort(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>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='mb-4'>
|
||||
<h4 className='text-sm font-medium text-gray-200 mb-2 flex items-center'>
|
||||
{icon}
|
||||
<span>
|
||||
{isIncoming
|
||||
? title
|
||||
: isDevMode
|
||||
? 'Outgoing Changes'
|
||||
: 'Local Changes'}{' '}
|
||||
({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) => (
|
||||
<ChangeRow
|
||||
key={`${
|
||||
isIncoming ? 'incoming' : 'outgoing'
|
||||
}-${index}`}
|
||||
change={change}
|
||||
isSelected={selectedChanges.includes(
|
||||
change.file_path
|
||||
)}
|
||||
onSelect={filePath =>
|
||||
onSelectChange(filePath, isIncoming)
|
||||
}
|
||||
isIncoming={isIncoming}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeTable;
|
||||
@@ -1,22 +1,22 @@
|
||||
import React, {useState} from 'react';
|
||||
import {Loader, Unlink, Link} from 'lucide-react';
|
||||
import React, { useState } from 'react';
|
||||
import { Loader, Unlink, Link } from 'lucide-react';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import {unlinkRepo, getSettings} from '../../../api/api';
|
||||
import { unlinkRepo, getSettings } from '../../../api/api';
|
||||
import Alert from '../../ui/Alert';
|
||||
import LinkRepo from './modal/LinkRepo';
|
||||
import UnlinkRepo from './modal/UnlinkRepo';
|
||||
|
||||
const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
|
||||
const RepoContainer = ({ settings, setSettings, fetchGitStatus }) => {
|
||||
const [loadingAction, setLoadingAction] = useState('');
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [showUnlinkRepo, setShowUnlinkRepo] = useState(false);
|
||||
|
||||
const handleLinkRepo = async () => {
|
||||
const handleLinkRepo = () => {
|
||||
setLoadingAction('link_repo');
|
||||
setShowLinkModal(true);
|
||||
};
|
||||
|
||||
const handleUnlinkRepo = async removeFiles => {
|
||||
const handleUnlinkRepo = async (removeFiles) => {
|
||||
setLoadingAction('unlink_repo');
|
||||
try {
|
||||
const response = await unlinkRepo(removeFiles);
|
||||
@@ -47,23 +47,17 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!settings) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleLinkRepo}
|
||||
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-sm font-medium shadow-md'>
|
||||
<Link size={16} className='mr-2' />
|
||||
Link Repository
|
||||
</button>
|
||||
<LinkRepo
|
||||
isOpen={showLinkModal}
|
||||
onClose={() => setShowLinkModal(false)}
|
||||
onSubmit={onLinkSubmit}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
// Handler to close the LinkRepo modal and reset loadingAction
|
||||
const closeLinkModal = () => {
|
||||
setShowLinkModal(false);
|
||||
setLoadingAction('');
|
||||
};
|
||||
|
||||
// Handler to close the UnlinkRepo modal and reset loadingAction
|
||||
const closeUnlinkModal = () => {
|
||||
setShowUnlinkRepo(false);
|
||||
setLoadingAction('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='space-y-4 mb-4'>
|
||||
@@ -71,37 +65,43 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {
|
||||
<div className='flex flex-col sm:flex-row items-start sm:items-center justify-between space-y-3 sm:space-y-0'>
|
||||
<div className='flex flex-col sm:flex-row sm:items-center'>
|
||||
<h3 className='text-sm font-semibold text-gray-100 mr-2 mb-1 sm:mb-0'>
|
||||
Connected Repository:
|
||||
{settings ? 'Connected Repository:' : 'Repository:'}
|
||||
</h3>
|
||||
<a
|
||||
href={settings.gitRepo}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-400 hover:text-blue-300 transition-colors text-sm font-medium truncate max-w-xs sm:max-w-md'>
|
||||
{settings.gitRepo}
|
||||
</a>
|
||||
{settings ? (
|
||||
<a
|
||||
href={settings.gitRepo}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='text-blue-400 hover:text-blue-300 transition-colors text-sm font-medium truncate max-w-xs sm:max-w-md'>
|
||||
{settings.gitRepo}
|
||||
</a>
|
||||
) : (
|
||||
<span className='text-gray-400 text-sm'>No repository linked</span>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip content='Unlink Repository'>
|
||||
<Tooltip content={settings ? 'Unlink Repository' : 'Link Repository'}>
|
||||
<button
|
||||
onClick={() => setShowUnlinkRepo(true)}
|
||||
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-sm font-medium shadow-sm'
|
||||
disabled={loadingAction === 'unlink_repo'}>
|
||||
{loadingAction === 'unlink_repo' ? (
|
||||
<Loader
|
||||
size={16}
|
||||
className='animate-spin mr-2'
|
||||
/>
|
||||
) : (
|
||||
onClick={settings ? () => setShowUnlinkRepo(true) : handleLinkRepo}
|
||||
className={`flex items-center px-4 py-2 ${settings ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'} text-white rounded-md transition-colors duration-200 ease-in-out text-sm font-medium shadow-sm`}
|
||||
disabled={loadingAction !== ''}>
|
||||
{settings ? (
|
||||
<Unlink size={16} className='mr-2' />
|
||||
) : (
|
||||
<Link size={16} className='mr-2' />
|
||||
)}
|
||||
Unlink
|
||||
{settings ? 'Unlink' : 'Link Repository'}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<LinkRepo
|
||||
isOpen={showLinkModal}
|
||||
onClose={closeLinkModal}
|
||||
onSubmit={onLinkSubmit}
|
||||
/>
|
||||
<UnlinkRepo
|
||||
isOpen={showUnlinkRepo}
|
||||
onClose={() => setShowUnlinkRepo(false)}
|
||||
onClose={closeUnlinkModal}
|
||||
onSubmit={handleUnlinkRepo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
274
frontend/src/components/settings/git/StatusContainer.jsx
Normal file
274
frontend/src/components/settings/git/StatusContainer.jsx
Normal file
@@ -0,0 +1,274 @@
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
GitBranch,
|
||||
Loader,
|
||||
RotateCcw,
|
||||
Download,
|
||||
CheckCircle,
|
||||
Plus,
|
||||
Eye
|
||||
} from 'lucide-react';
|
||||
import ChangeTable from './ChangeTable';
|
||||
import Tooltip from '../../ui/Tooltip';
|
||||
import CommitSection from '../CommitSection';
|
||||
|
||||
const StatusContainer = ({
|
||||
status,
|
||||
isDevMode,
|
||||
onViewBranches,
|
||||
onStageSelected,
|
||||
onCommitSelected,
|
||||
onRevertSelected,
|
||||
onPullSelected,
|
||||
loadingAction
|
||||
}) => {
|
||||
const [sortConfig, setSortConfig] = useState({
|
||||
key: 'type',
|
||||
direction: 'ascending'
|
||||
});
|
||||
const [selectedIncomingChanges, setSelectedIncomingChanges] = useState([]);
|
||||
const [selectedOutgoingChanges, setSelectedOutgoingChanges] = useState([]);
|
||||
const [commitMessage, setCommitMessage] = useState('');
|
||||
const [selectionType, setSelectionType] = useState(null);
|
||||
|
||||
const requestSort = key => {
|
||||
let direction = 'ascending';
|
||||
if (sortConfig.key === key && sortConfig.direction === 'ascending') {
|
||||
direction = 'descending';
|
||||
}
|
||||
setSortConfig({key, direction});
|
||||
};
|
||||
|
||||
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 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';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded-md'>
|
||||
<h3 className='text-sm font-semibold text-gray-100 mb-2'>
|
||||
Git Status
|
||||
</h3>
|
||||
<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={onViewBranches}
|
||||
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 && (
|
||||
<ChangeTable
|
||||
changes={status.incoming_changes}
|
||||
title='Incoming Changes'
|
||||
icon={
|
||||
<Download className='text-yellow-400 mr-2' size={16} />
|
||||
}
|
||||
isIncoming={true}
|
||||
selectedChanges={selectedIncomingChanges}
|
||||
onSelectChange={handleSelectChange}
|
||||
sortConfig={sortConfig}
|
||||
onRequestSort={requestSort}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status.outgoing_changes.length > 0 && (
|
||||
<ChangeTable
|
||||
changes={status.outgoing_changes}
|
||||
title='Outgoing Changes'
|
||||
icon={
|
||||
<GitBranch className='text-blue-400 mr-2' size={16} />
|
||||
}
|
||||
isIncoming={false}
|
||||
selectedChanges={selectedOutgoingChanges}
|
||||
onSelectChange={handleSelectChange}
|
||||
sortConfig={sortConfig}
|
||||
onRequestSort={requestSort}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CommitSection
|
||||
status={status}
|
||||
commitMessage={commitMessage}
|
||||
setCommitMessage={setCommitMessage}
|
||||
loadingAction={loadingAction}
|
||||
hasIncomingChanges={status.incoming_changes.length > 0}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
{isDevMode && (
|
||||
<>
|
||||
{selectedOutgoingChanges.length > 0 &&
|
||||
selectionType !== 'staged' && (
|
||||
<Tooltip content={getStageButtonTooltip()}>
|
||||
<button
|
||||
onClick={() =>
|
||||
onStageSelected(
|
||||
selectedOutgoingChanges
|
||||
)
|
||||
}
|
||||
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>
|
||||
)}
|
||||
|
||||
{selectedOutgoingChanges.length > 0 &&
|
||||
commitMessage.trim() &&
|
||||
selectionType !== 'unstaged' && (
|
||||
<Tooltip content={getCommitButtonTooltip()}>
|
||||
<button
|
||||
onClick={() =>
|
||||
onCommitSelected(
|
||||
selectedOutgoingChanges,
|
||||
commitMessage
|
||||
)
|
||||
}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedOutgoingChanges.length > 0 && (
|
||||
<Tooltip content={getRevertButtonTooltip()}>
|
||||
<button
|
||||
onClick={() =>
|
||||
onRevertSelected(selectedOutgoingChanges)
|
||||
}
|
||||
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>
|
||||
)}
|
||||
|
||||
{selectedIncomingChanges.length > 0 && (
|
||||
<Tooltip content='Pull selected changes'>
|
||||
<button
|
||||
onClick={() =>
|
||||
onPullSelected(selectedIncomingChanges)
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusContainer;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {useState} from 'react';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Modal from '../../../ui/Modal';
|
||||
import {Loader} from 'lucide-react';
|
||||
import {cloneRepo} from '../../../api/api';
|
||||
import Alert from '../../ui/Alert';
|
||||
import {cloneRepo} from '../../../../api/api';
|
||||
import Alert from '../../../ui/Alert';
|
||||
|
||||
const LinkRepo = ({isOpen, onClose, onSubmit}) => {
|
||||
const [gitRepo, setGitRepo] = useState('');
|
||||
|
||||
100
frontend/src/components/settings/git/modal/ViewDiff.jsx
Normal file
100
frontend/src/components/settings/git/modal/ViewDiff.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from '../../../ui/Modal';
|
||||
|
||||
const Diff = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
diffContent,
|
||||
type,
|
||||
name,
|
||||
commitMessage,
|
||||
title = 'View Diff'
|
||||
}) => {
|
||||
const formatDiffContent = content => {
|
||||
if (!content) return [];
|
||||
return content.split('\n').map((line, index) => {
|
||||
let lineClass = 'py-1 pl-4 border-l-2 ';
|
||||
if (line.startsWith('+')) {
|
||||
lineClass += 'bg-green-900/30 text-green-400 border-green-500';
|
||||
} else if (line.startsWith('-')) {
|
||||
lineClass += 'bg-red-900/30 text-red-400 border-red-500';
|
||||
} else {
|
||||
lineClass += 'border-transparent';
|
||||
}
|
||||
return (
|
||||
<div key={index} className={`flex ${lineClass}`}>
|
||||
<span className='w-12 text-gray-500 select-none text-right pr-4 border-r border-gray-700'>
|
||||
{index + 1}
|
||||
</span>
|
||||
<code className='flex-1 pl-4 font-mono text-sm'>
|
||||
{line}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const formattedContent = formatDiffContent(diffContent);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={title} size='4xl'>
|
||||
<div className='space-y-4'>
|
||||
<div className='bg-gray-100 dark:bg-gray-700 p-4 rounded-lg space-y-2 text-sm'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<span className='font-medium text-gray-600 dark:text-gray-300'>
|
||||
Type:
|
||||
</span>
|
||||
<span className='bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded'>
|
||||
{type}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex justify-between items-center'>
|
||||
<span className='font-medium text-gray-600 dark:text-gray-300'>
|
||||
Name:
|
||||
</span>
|
||||
<span className='bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 px-2 py-1 rounded'>
|
||||
{name === 'Deleted File' ? 'Deleted File' : name}
|
||||
</span>
|
||||
</div>
|
||||
{commitMessage && (
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-medium text-gray-600 dark:text-gray-300 mb-1'>
|
||||
Commit Message:
|
||||
</span>
|
||||
<p className='bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 p-2 rounded'>
|
||||
{commitMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden'>
|
||||
<div className='bg-gray-50 dark:bg-gray-800 p-2 text-sm font-medium text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600'>
|
||||
Diff Content
|
||||
</div>
|
||||
<div className='bg-white dark:bg-gray-900 p-4 max-h-[60vh] overflow-y-auto'>
|
||||
{formattedContent.length > 0 ? (
|
||||
formattedContent
|
||||
) : (
|
||||
<div className='text-gray-500 dark:text-gray-400 italic'>
|
||||
No differences found or file is empty.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
Diff.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
diffContent: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
commitMessage: PropTypes.string,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export default Diff;
|
||||
31
frontend/src/utils/messages.js
Normal file
31
frontend/src/utils/messages.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// messages.js
|
||||
|
||||
export const statusLoadingMessages = [
|
||||
"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..."
|
||||
];
|
||||
|
||||
export const noChangesMessages = [
|
||||
'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. 🎬👑'
|
||||
];
|
||||
|
||||
export const getRandomMessage = (messages) => {
|
||||
return messages[Math.floor(Math.random() * messages.length)];
|
||||
};
|
||||
Reference in New Issue
Block a user