diff --git a/frontend/src/components/settings/CommitSection.jsx b/frontend/src/components/settings/CommitSection.jsx index 100ef1c..7d9e12a 100644 --- a/frontend/src/components/settings/CommitSection.jsx +++ b/frontend/src/components/settings/CommitSection.jsx @@ -38,12 +38,6 @@ const CommitSection = ({ /> )} - {hasUnstagedChanges && !hasStagedChanges && ( -

- You have unstaged changes. Stage your - changes before committing. -

- )} ) : (
diff --git a/frontend/src/components/settings/DiffModal.jsx b/frontend/src/components/settings/DiffModal.jsx deleted file mode 100644 index d58c800..0000000 --- a/frontend/src/components/settings/DiffModal.jsx +++ /dev/null @@ -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 ( -
- - {index + 1} - - {line} -
- ); - }); - }; - - const formattedContent = formatDiffContent(diffContent); - - return ( - -
-
-
- - Type: - - - {type} - -
-
- - Name: - - - {name === "Deleted File" ? "Deleted File" : name} - -
- {commitMessage && ( -
- - Commit Message: - -

- {commitMessage} -

-
- )} -
-
-
- Diff Content -
-
- {formattedContent.length > 0 ? ( - formattedContent - ) : ( -
- No differences found or file is empty. -
- )} -
-
-
-
- ); -}; - -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; diff --git a/frontend/src/components/settings/SettingsPage.jsx b/frontend/src/components/settings/SettingsPage.jsx index 905504e..ef283a7 100644 --- a/frontend/src/components/settings/SettingsPage.jsx +++ b/frontend/src/components/settings/SettingsPage.jsx @@ -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 ( - requestSort(sortKey)}> -
- {children} - {isSorted && - (sortConfig.direction === 'ascending' ? ( - - ) : ( - - ))} -
- - ); - }; - 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) => ( -
-

- {icon} - - {isIncoming - ? title - : isDevMode - ? 'Outgoing Changes' - : 'Local Changes'}{' '} - ({changes.length}) - -

-
- - - - - Status - - Type - Name - - - - - - {sortedChanges(changes).map((change, index) => ( - - handleSelectChange( - change.file_path, - isIncoming - ) - }> - - - - - - - ))} - -
- Actions - - Select -
-
- {getStatusIcon(change.status)} - - {change.staged - ? `${change.status} (Staged)` - : change.status} - -
-
-
- {getTypeIcon(change.type)} - - {change.type} - -
-
- {change.name || 'Unnamed'} - - - - - - e.stopPropagation()} - disabled={!isIncoming && change.staged} - /> -
-
-
- ); - - 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 ; - case 'Staged (New)': - return ; - case 'Staged (Modified)': - case 'Modified': - return ; - case 'Deleted': - return ; - case 'Deleted (Staged)': - return ; - case 'Renamed': - return ; - default: - return ; - } - }; - - const getTypeIcon = type => { - switch (type) { - case 'Regex Pattern': - return ; - case 'Custom Format': - return ; - case 'Quality Profile': - return ; - default: - return ; - } - }; - return (
-

- Git Repository Settings - +

+ Git Settings + {isDevMode ? 'Dev Mode: Enabled' : 'Dev Mode: Disabled'}

@@ -574,248 +169,48 @@ const SettingsPage = () => { /> {settings && ( -
-
-

- Git Status -

- {loadingStatus ? ( -
- - - {loadingMessage} - -
- ) : ( - status && ( - <> -
-
- - - Current Branch: {status.branch} - -
- -
- - {status.incoming_changes.length > 0 && - renderChangeTable( - status.incoming_changes, - 'Incoming Changes', - , - true - )} - {status.outgoing_changes.length > 0 && - renderChangeTable( - status.outgoing_changes, - 'Outgoing Changes', - , - false - )} - 0 - } - funMessage={funMessage} - isDevMode={isDevMode} - /> - {/* Buttons Below Commit Section */} -
- {isDevMode && ( - <> - {/* Stage button */} - {selectedOutgoingChanges.length > - 0 && - selectionType !== - 'staged' && ( - - - - )} - - {/* Commit button */} - {selectedOutgoingChanges.length > - 0 && - commitMessage.trim() && - selectionType !== - 'unstaged' && ( - - - - )} - - )} - - {/* Revert button (moved outside isDevMode check) */} - {selectedOutgoingChanges.length > 0 && ( - - - - )} - - {/* Pull button (always enabled) */} - {selectedIncomingChanges.length > 0 && ( - - - - )} -
- - ) - )} -
+
+ {statusLoading ? ( +
+ + {statusLoadingMessage} +
+ ) : changes && (changes.incoming_changes.length > 0 || changes.outgoing_changes.length > 0) ? ( + setShowBranchModal(true)} + onStageSelected={handleStageSelectedChanges} + onCommitSelected={handleCommitSelectedChanges} + onRevertSelected={handleRevertSelectedChanges} + onPullSelected={handlePullSelectedChanges} + loadingAction={loadingAction} + /> + ) : ( +
+ {noChangesMessage} +
+ )}
)} -

+ +

Arr Management

- {settings && status && ( + + {settings && changes && ( setShowBranchModal(false)} repoUrl={settings.gitRepo} - currentBranch={status.branch} + currentBranch={changes.branch} onBranchChange={fetchGitStatus} isDevMode={isDevMode} /> )} - {showDiffModal && currentChange && ( - setShowDiffModal(false)} - diffContent={diffContent} - type={currentChange.type} - name={currentChange.name} - commitMessage={currentChange.commit_message} - isDevMode={isDevMode} - /> - )}
); }; -export default SettingsPage; +export default SettingsPage; \ No newline at end of file diff --git a/frontend/src/components/settings/git/ChangeRow.jsx b/frontend/src/components/settings/git/ChangeRow.jsx new file mode 100644 index 0000000..f09e72d --- /dev/null +++ b/frontend/src/components/settings/git/ChangeRow.jsx @@ -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 ; + case 'Staged (New)': + return ; + case 'Staged (Modified)': + case 'Modified': + return ; + case 'Deleted': + return ; + case 'Deleted (Staged)': + return ; + case 'Renamed': + return ; + default: + return ; + } + }; + + const getTypeIcon = type => { + switch (type) { + case 'Regex Pattern': + return ; + case 'Custom Format': + return ; + case 'Quality Profile': + return ; + default: + return ; + } + }; + + 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 ( + <> + onSelect(change.file_path)}> + +
+ {getStatusIcon(change.status)} + + {change.staged + ? `${change.status} (Staged)` + : change.status} + +
+ + +
+ {getTypeIcon(change.type)} + {change.type} +
+ + + {change.name || 'Unnamed'} + + + + + + + + e.stopPropagation()} + disabled={!isIncoming && change.staged} + /> + + + {showDiff && ( + setShowDiff(false)} + diffContent={diffContent} + type={change.type} + name={change.name} + commitMessage={change.commit_message} + isDevMode={isDevMode} + /> + )} + + ); +}; + +export default ChangeRow; diff --git a/frontend/src/components/settings/git/ChangeTable.jsx b/frontend/src/components/settings/git/ChangeTable.jsx new file mode 100644 index 0000000..8d62d91 --- /dev/null +++ b/frontend/src/components/settings/git/ChangeTable.jsx @@ -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 ( + onRequestSort(sortKey)}> +
+ {children} + {isSorted && + (sortConfig.direction === 'ascending' ? ( + + ) : ( + + ))} +
+ + ); + }; + + return ( +
+

+ {icon} + + {isIncoming + ? title + : isDevMode + ? 'Outgoing Changes' + : 'Local Changes'}{' '} + ({changes.length}) + +

+
+ + + + + Status + + Type + Name + + + + + + {sortedChanges(changes).map((change, index) => ( + + onSelectChange(filePath, isIncoming) + } + isIncoming={isIncoming} + isDevMode={isDevMode} + /> + ))} + +
+ Actions + + Select +
+
+
+ ); +}; + +export default ChangeTable; diff --git a/frontend/src/components/settings/git/RepoContainer.jsx b/frontend/src/components/settings/git/RepoContainer.jsx index 8b5253f..98cee29 100644 --- a/frontend/src/components/settings/git/RepoContainer.jsx +++ b/frontend/src/components/settings/git/RepoContainer.jsx @@ -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 ( - <> - - 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 (
@@ -71,37 +65,43 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus}) => {

- Connected Repository: + {settings ? 'Connected Repository:' : 'Repository:'}

- - {settings.gitRepo} - + {settings ? ( + + {settings.gitRepo} + + ) : ( + No repository linked + )}
- +
+ setShowUnlinkRepo(false)} + onClose={closeUnlinkModal} onSubmit={handleUnlinkRepo} />

diff --git a/frontend/src/components/settings/git/StatusContainer.jsx b/frontend/src/components/settings/git/StatusContainer.jsx new file mode 100644 index 0000000..4547834 --- /dev/null +++ b/frontend/src/components/settings/git/StatusContainer.jsx @@ -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 ( +
+

+ Git Status +

+
+
+ + + Current Branch: {status.branch} + +
+ +
+ + {status.incoming_changes.length > 0 && ( + + } + isIncoming={true} + selectedChanges={selectedIncomingChanges} + onSelectChange={handleSelectChange} + sortConfig={sortConfig} + onRequestSort={requestSort} + isDevMode={isDevMode} + /> + )} + + {status.outgoing_changes.length > 0 && ( + + } + isIncoming={false} + selectedChanges={selectedOutgoingChanges} + onSelectChange={handleSelectChange} + sortConfig={sortConfig} + onRequestSort={requestSort} + isDevMode={isDevMode} + /> + )} + + 0} + isDevMode={isDevMode} + /> + + {/* Action Buttons */} +
+ {isDevMode && ( + <> + {selectedOutgoingChanges.length > 0 && + selectionType !== 'staged' && ( + + + + )} + + {selectedOutgoingChanges.length > 0 && + commitMessage.trim() && + selectionType !== 'unstaged' && ( + + + + )} + + )} + + {selectedOutgoingChanges.length > 0 && ( + + + + )} + + {selectedIncomingChanges.length > 0 && ( + + + + )} +
+
+ ); +}; + +export default StatusContainer; diff --git a/frontend/src/components/settings/git/modal/LinkRepo.jsx b/frontend/src/components/settings/git/modal/LinkRepo.jsx index f663a70..af62642 100644 --- a/frontend/src/components/settings/git/modal/LinkRepo.jsx +++ b/frontend/src/components/settings/git/modal/LinkRepo.jsx @@ -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(''); diff --git a/frontend/src/components/settings/git/modal/ViewDiff.jsx b/frontend/src/components/settings/git/modal/ViewDiff.jsx new file mode 100644 index 0000000..be7d1ea --- /dev/null +++ b/frontend/src/components/settings/git/modal/ViewDiff.jsx @@ -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 ( +
+ + {index + 1} + + + {line} + +
+ ); + }); + }; + + const formattedContent = formatDiffContent(diffContent); + + return ( + +
+
+
+ + Type: + + + {type} + +
+
+ + Name: + + + {name === 'Deleted File' ? 'Deleted File' : name} + +
+ {commitMessage && ( +
+ + Commit Message: + +

+ {commitMessage} +

+
+ )} +
+
+
+ Diff Content +
+
+ {formattedContent.length > 0 ? ( + formattedContent + ) : ( +
+ No differences found or file is empty. +
+ )} +
+
+
+
+ ); +}; + +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; diff --git a/frontend/src/utils/messages.js b/frontend/src/utils/messages.js new file mode 100644 index 0000000..6100989 --- /dev/null +++ b/frontend/src/utils/messages.js @@ -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)]; +};