import React, {useState, useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import FormatCard from './FormatCard';
import FormatModal from './FormatModal';
import {getGitStatus} from '@api/api';
import {CustomFormats} from '@api/data';
import {Loader} from 'lucide-react';
import Alert from '@ui/Alert';
import {useFormatModal} from '@hooks/useFormatModal';
import {useMassSelection} from '@hooks/useMassSelection';
import {useKeyboardShortcut} from '@hooks/useKeyboardShortcut';
import MassActionsBar from '@ui/MassActionsBar';
import ImportModal from '@ui/ImportModal';
import {importFormats} from '@api/import';
import DataBar from '@ui/DataBar/DataBar';
import useSearch from '@hooks/useSearch';
const loadingMessages = [
'Formatting the formatters...',
'Teaching formats to behave...',
'Convincing formats to follow rules...',
'Organizing chaos into patterns...',
'Making formats look pretty...',
'Polishing the quality filters...'
];
const LoadingState = () => (
{
loadingMessages[
Math.floor(Math.random() * loadingMessages.length)
]
}
);
const ConflictState = ({onNavigateSettings}) => (
Merge Conflicts Detected
Resolve Conflicts
What Happened?
This page is locked because there are unresolved merge
conflicts. You need to address these conflicts in the settings
page before continuing.
);
function FormatPage() {
const [formats, setFormats] = useState([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedFormat, setSelectedFormat] = useState(null);
const [allTags, setAllTags] = useState([]);
const [isCloning, setIsCloning] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [mergeConflicts, setMergeConflicts] = useState([]);
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const [willBeSelected, setWillBeSelected] = useState([]);
const navigate = useNavigate();
// Initialize useSearch hook
const {
searchTerms,
currentInput,
setCurrentInput,
addSearchTerm,
removeSearchTerm,
clearSearchTerms,
filterType,
setFilterType,
filterValue,
setFilterValue,
sortBy,
setSortBy,
items: filteredFormats
} = useSearch(formats, {
searchableFields: ['content.name', 'content.tags'],
initialSortBy: 'name',
sortOptions: {
name: (a, b) => a.content.name.localeCompare(b.content.name),
dateModified: (a, b) =>
new Date(b.modified_date) - new Date(a.modified_date)
}
});
const {
selectedItems,
isSelectionMode,
toggleSelectionMode,
handleSelect,
clearSelection,
lastSelectedIndex
} = useMassSelection();
useKeyboardShortcut('m', toggleSelectionMode, {ctrl: true});
const {
name,
description,
tags,
conditions,
tests,
error,
activeTab,
isDeleting,
isRunningTests,
includeInRename,
setName,
setDescription,
setTags,
setConditions,
setTests,
setActiveTab,
setIsDeleting,
setIncludeInRename,
initializeForm,
handleSave,
handleRunTests,
handleDelete
} = useFormatModal(selectedFormat, () => {
fetchFormats();
handleCloseModal();
});
useEffect(() => {
fetchGitStatus();
}, []);
useEffect(() => {
const handleKeyDown = e => {
if (e.key === 'Shift' && lastSelectedIndex !== null) {
const element = document.elementFromPoint(
window.mouseX,
window.mouseY
);
if (element) {
const card = element.closest('[data-format-index]');
if (card) {
const index = parseInt(card.dataset.formatIndex);
handleMouseEnter(index, true);
}
}
}
};
const handleKeyUp = e => {
if (e.key === 'Shift') {
setWillBeSelected([]);
}
};
const handleMouseMove = e => {
window.mouseX = e.clientX;
window.mouseY = e.clientY;
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
window.removeEventListener('mousemove', handleMouseMove);
};
}, [lastSelectedIndex]);
const fetchGitStatus = async () => {
try {
const result = await getGitStatus();
if (result.success) {
setMergeConflicts(result.data.merge_conflicts || []);
if (result.data.merge_conflicts.length === 0) {
fetchFormats();
} else {
setIsLoading(false);
}
}
} catch (error) {
console.error('Error fetching Git status:', error);
Alert.error('Failed to check repository status');
setIsLoading(false);
}
};
const fetchFormats = async () => {
try {
const response = await CustomFormats.getAll();
const formatsData = response.map(item => ({
file_name: item.file_name,
modified_date: item.modified_date,
content: {
...item.content
}
}));
setFormats(formatsData);
const tags = new Set(
formatsData.flatMap(format => format.content.tags || [])
);
setAllTags(Array.from(tags));
} catch (error) {
console.error('Error fetching formats:', error);
Alert.error('Failed to load custom formats');
} finally {
setIsLoading(false);
}
};
const handleOpenModal = (format = null) => {
if (isSelectionMode) return;
setSelectedFormat(format);
setIsModalOpen(true);
setIsCloning(false);
initializeForm(format?.content, false);
};
const handleCloseModal = () => {
setSelectedFormat(null);
setIsModalOpen(false);
setIsCloning(false);
};
const handleCloneFormat = format => {
if (isSelectionMode) return;
const clonedFormat = {
...format,
content: {
...format.content,
name: `${format.content.name} [COPY]`
}
};
setSelectedFormat(clonedFormat);
setIsModalOpen(true);
setIsCloning(true);
initializeForm(clonedFormat.content, true);
};
const handleMassDelete = async () => {
try {
const selectedFormats = Array.from(selectedItems).map(
index => filteredFormats[index]
);
for (const format of selectedFormats) {
await CustomFormats.delete(
format.file_name.replace('.yml', '')
);
}
Alert.success('Selected formats deleted successfully');
} catch (error) {
console.error('Error deleting formats:', error);
Alert.error(
error.response?.data?.error ||
'Failed to delete selected formats'
);
} finally {
fetchFormats();
toggleSelectionMode();
clearSelection();
}
};
const handleMassImport = async arrID => {
try {
const selectedFormats = Array.from(selectedItems).map(
index => filteredFormats[index]
);
const formatNames = selectedFormats.map(format => format.file_name);
const result = await importFormats(arrID, formatNames);
if (result.status === 'partial') {
const { added, updated, failed } = result;
Alert.partial(
`Import partially successful:\n- ${added} added\n- ${updated} updated\n- ${failed} failed`
);
} else {
Alert.success('Formats imported successfully');
}
toggleSelectionMode();
} catch (error) {
console.error('Error importing formats:', error);
Alert.error('Failed to import formats');
throw error;
}
};
const handleFormatSelect = (formatName, index, e) => {
if (e.shiftKey) {
handleMouseEnter(index, true);
}
handleSelect(formatName, index, e, filteredFormats);
};
const handleMouseEnter = (index, isShiftKey) => {
if (isShiftKey && lastSelectedIndex !== null) {
const start = Math.min(lastSelectedIndex, index);
const end = Math.max(lastSelectedIndex, index);
const potentialSelection = filteredFormats
.slice(start, end + 1)
.map((format, idx) => idx + start);
setWillBeSelected(potentialSelection);
}
};
if (isLoading) {
return ;
}
if (mergeConflicts.length > 0) {
return (
navigate('/settings')} />
);
}
return (
handleOpenModal()}
addButtonLabel='Add New Format'
/>
{filteredFormats.map((format, index) => (
handleMouseEnter(index, window.event?.shiftKey)
}
onMouseLeave={() => setWillBeSelected([])}>
handleOpenModal(format)}
onClone={handleCloneFormat}
sortBy={sortBy}
isSelectionMode={isSelectionMode}
isSelected={selectedItems.has(index)}
willBeSelected={willBeSelected.includes(index)}
onSelect={e =>
handleFormatSelect(
format.content.name,
index,
e
)
}
/>
))}
{isSelectionMode && (
{
toggleSelectionMode();
clearSelection();
}}
onDelete={handleMassDelete}
onImport={() => setIsImportModalOpen(true)}
/>
)}
setIsImportModalOpen(false)}
onImport={handleMassImport}
type='Formats'
/>
);
}
export default FormatPage;