diff --git a/backend/app/data_operations.py b/backend/app/data_operations.py new file mode 100644 index 0000000..8b50a7b --- /dev/null +++ b/backend/app/data_operations.py @@ -0,0 +1,24 @@ +import os +import yaml + +DATA_DIR = '/app/data' +FORMAT_DIR = os.path.join(DATA_DIR, 'db', 'custom_formats') +PROFILE_DIR = os.path.join(DATA_DIR, 'db', 'profiles') + +def load_all_formats(): + formats = [] + for filename in os.listdir(FORMAT_DIR): + if filename.endswith('.yml'): + with open(os.path.join(FORMAT_DIR, filename), 'r') as file: + data = yaml.safe_load(file) + formats.append(data) + return formats + +def load_all_profiles(): + profiles = [] + for filename in os.listdir(PROFILE_DIR): + if filename.endswith('.yml'): + with open(os.path.join(PROFILE_DIR, filename), 'r') as file: + data = yaml.safe_load(file) + profiles.append(data) + return profiles \ No newline at end of file diff --git a/backend/app/format.py b/backend/app/format.py index 7703cb6..5a4c9b7 100644 --- a/backend/app/format.py +++ b/backend/app/format.py @@ -4,6 +4,7 @@ import os import yaml import logging from .utils import get_next_id, generate_filename, get_current_timestamp, sanitize_input +from .data_operations import load_all_profiles, load_all_formats bp = Blueprint('format', __name__, url_prefix='/format') DATA_DIR = '/app/data' @@ -38,9 +39,18 @@ def handle_format(id): saved_data = save_format(data) return jsonify(saved_data) elif request.method == 'DELETE': - if delete_format(id): - return jsonify({"message": f"Format with ID {id} deleted."}), 200 - return jsonify({"error": f"Format with ID {id} not found."}), 404 + result = delete_format(id) + if "error" in result: + return jsonify(result), 400 + return jsonify(result), 200 + +def is_format_used_in_profile(format_id): + profiles = load_all_profiles() + for profile in profiles: + for custom_format in profile.get('custom_formats', []): + if custom_format.get('id') == format_id and custom_format.get('score', 0) != 0: + return True + return False def save_format(data): logger.info("Received data for saving format: %s", data) @@ -115,19 +125,12 @@ def load_format(id): return data return None - -def load_all_formats(): - formats = [] - for filename in os.listdir(FORMAT_DIR): - if filename.endswith('.yml'): - with open(os.path.join(FORMAT_DIR, filename), 'r') as file: - data = yaml.safe_load(file) - formats.append(data) - return formats - def delete_format(id): + if is_format_used_in_profile(id): + return {"error": "Format in use", "message": "This format is being used in one or more profiles."} + filename = os.path.join(FORMAT_DIR, f"{id}.yml") if os.path.exists(filename): os.remove(filename) - return True - return False + return {"message": f"Format with ID {id} deleted."} + return {"error": f"Format with ID {id} not found."} \ No newline at end of file diff --git a/backend/app/profile.py b/backend/app/profile.py index e1a48f2..a822d4d 100644 --- a/backend/app/profile.py +++ b/backend/app/profile.py @@ -4,6 +4,7 @@ import os import yaml import logging from .utils import get_next_id, generate_filename, get_current_timestamp, sanitize_input +from .data_operations import load_all_profiles, load_all_formats bp = Blueprint('profile', __name__, url_prefix='/profile') DATA_DIR = '/app/data' @@ -68,6 +69,20 @@ def save_profile(data): # Process tags tags = [sanitize_input(tag) for tag in data.get('tags', [])] + # Get all existing formats + all_formats = load_all_formats() + + # Process custom formats + custom_formats = {format['id']: format['score'] for format in data.get('custom_formats', [])} + + # Ensure all formats are included with a minimum score of 0 + final_custom_formats = [] + for format in all_formats: + final_custom_formats.append({ + 'id': format['id'], + 'score': max(custom_formats.get(format['id'], 0), 0) # Ensure minimum score of 0 + }) + # Construct the ordered data ordered_data = OrderedDict([ ('id', profile_id), @@ -75,7 +90,8 @@ def save_profile(data): ('description', description), ('date_created', str(date_created)), ('date_modified', str(date_modified)), - ('tags', tags) + ('tags', tags), + ('custom_formats', final_custom_formats) ]) # Generate the filename using only the ID @@ -95,15 +111,6 @@ def load_profile(id): return data return None -def load_all_profiles(): - profiles = [] - for filename in os.listdir(PROFILE_DIR): - if filename.endswith('.yml'): - with open(os.path.join(PROFILE_DIR, filename), 'r') as file: - data = yaml.safe_load(file) - profiles.append(data) - return profiles - def delete_profile(id): filename = os.path.join(PROFILE_DIR, f"{id}.yml") if os.path.exists(filename): diff --git a/backend/app/regex.py b/backend/app/regex.py index 1ad21c2..f6729b7 100644 --- a/backend/app/regex.py +++ b/backend/app/regex.py @@ -6,6 +6,7 @@ import json import logging import subprocess from .utils import get_next_id, generate_filename, get_current_timestamp, sanitize_input +from .format import load_all_formats bp = Blueprint('regex', __name__, url_prefix='/regex') DATA_DIR = '/app/data' @@ -88,9 +89,10 @@ def handle_regex(id): saved_data = save_regex(data) return jsonify(saved_data) elif request.method == 'DELETE': - if delete_regex(id): - return jsonify({"message": f"Regex with ID {id} deleted."}), 200 - return jsonify({"error": f"Regex with ID {id} not found."}), 404 + result = delete_regex(id) + if "error" in result: + return jsonify(result), 400 + return jsonify(result), 200 def save_regex(data): ordered_data = OrderedDict() @@ -145,6 +147,13 @@ def save_regex(data): return ordered_data +def is_regex_used_in_format(regex_id): + formats = load_all_formats() + for format in formats: + for condition in format.get('conditions', []): + if condition.get('type') == 'regex' and condition.get('regex_id') == regex_id: + return True + return False def find_existing_file(regex_id): """Find the existing filename for a given regex ID.""" @@ -174,8 +183,11 @@ def load_all_regexes(): return regexes def delete_regex(id): + if is_regex_used_in_format(id): + return {"error": "Regex in use", "message": "This regex is being used in one or more custom formats."} + filename = os.path.join(REGEX_DIR, f"{id}.yml") if os.path.exists(filename): os.remove(filename) - return True - return False + return {"message": f"Regex with ID {id} deleted."} + return {"error": f"Regex with ID {id} not found."} \ No newline at end of file diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 54debed..a7c909c 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -32,11 +32,11 @@ export const updateRegex = async (id, regex) => { } }; -export const deleteRegex = async (id) => { +export const deleteRegex = async (id, force = false) => { try { - const response = await axios.delete(`${API_BASE_URL}/regex/${id}`, { + const response = await axios.delete(`${API_BASE_URL}/regex/${id}${force ? '?force=true' : ''}`, { validateStatus: (status) => { - return status >= 200 && status < 300 || status === 409; // Accept 200-299 or 409 Conflict + return status >= 200 && status < 300 || status === 400 || status === 409; } }); return response.data; @@ -76,9 +76,13 @@ export const updateFormat = async (id, format) => { } }; -export const deleteFormat = async (id) => { +export const deleteFormat = async (id, force = false) => { try { - const response = await axios.delete(`${API_BASE_URL}/format/${id}`); + const response = await axios.delete(`${API_BASE_URL}/format/${id}${force ? '?force=true' : ''}`, { + validateStatus: (status) => { + return status >= 200 && status < 300 || status === 400 || status === 409; + } + }); return response.data; } catch (error) { console.error('Error deleting format:', error); diff --git a/frontend/src/components/format/FormatModal.jsx b/frontend/src/components/format/FormatModal.jsx index baa3dce..5177be0 100644 --- a/frontend/src/components/format/FormatModal.jsx +++ b/frontend/src/components/format/FormatModal.jsx @@ -1,80 +1,95 @@ -import { useState, useEffect, useRef } from 'react'; -import PropTypes from 'prop-types'; -import { saveFormat, deleteFormat, getRegexes } from '../../api/api'; -import ConditionModal from '../condition/ConditionModal'; -import ConditionCard from '../condition/ConditionCard'; -import Modal from '../ui/Modal'; +import { useState, useEffect, useRef } from "react"; +import PropTypes from "prop-types"; +import { saveFormat, deleteFormat, getRegexes } from "../../api/api"; +import ConditionModal from "../condition/ConditionModal"; +import ConditionCard from "../condition/ConditionCard"; +import Modal from "../ui/Modal"; +import Alert from "../ui/Alert"; -function FormatModal({ format = null, isOpen, onClose, onSave }) { - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); +function FormatModal({ format: initialFormat, isOpen, onClose, onSave }) { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); const [conditions, setConditions] = useState([]); const [isConditionModalOpen, setIsConditionModalOpen] = useState(false); const [selectedCondition, setSelectedCondition] = useState(null); const [regexes, setRegexes] = useState([]); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const [tags, setTags] = useState([]); - const [newTag, setNewTag] = useState(''); - const initialFormatRef = useRef(format); + const [newTag, setNewTag] = useState(""); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { if (isOpen) { - if (format && format.id !== 0) { // Check if this is an existing format - initialFormatRef.current = format; - setName(format.name); - setDescription(format.description); - setConditions(format.conditions || []); - setTags(format.tags || []); - } else { // This is a new format (or cloned) - initialFormatRef.current = null; - setName(format ? format.name : ''); - setDescription(format ? format.description : ''); - setConditions(format ? format.conditions : []); - setTags(format ? format.tags : []); + if (initialFormat && initialFormat.id !== 0) { + setName(initialFormat.name); + setDescription(initialFormat.description); + setConditions(initialFormat.conditions || []); + setTags(initialFormat.tags || []); + } else { + setName(""); + setDescription(""); + setConditions([]); + setTags([]); } - setError(''); - setNewTag(''); + setError(""); + setNewTag(""); + setIsDeleting(false); fetchRegexes(); } - }, [format, isOpen]); - + }, [initialFormat, isOpen]); const fetchRegexes = async () => { try { const fetchedRegexes = await getRegexes(); setRegexes(fetchedRegexes); } catch (error) { - console.error('Error fetching regexes:', error); - setError('Failed to fetch regexes. Please try again.'); + console.error("Error fetching regexes:", error); + setError("Failed to fetch regexes. Please try again."); } }; const handleSave = async () => { if (!name.trim() || !description.trim() || conditions.length === 0) { - setError('Name, description, and at least one condition are required.'); + setError("Name, description, and at least one condition are required."); return; } try { - await saveFormat({ id: initialFormatRef.current ? initialFormatRef.current.id : 0, name, description, conditions, tags }); + await saveFormat({ + id: initialFormat ? initialFormat.id : 0, + name, + description, + conditions, + tags, + }); onSave(); onClose(); } catch (error) { - console.error('Error saving format:', error); - setError('Failed to save format. Please try again.'); + console.error("Error saving format:", error); + setError("Failed to save format. Please try again."); } }; const handleDelete = async () => { - console.log(`Trying to delete format with id: ${initialFormatRef.current.id}`); - if (initialFormatRef.current && initialFormatRef.current.id) { + if (isDeleting) { try { - await deleteFormat(initialFormatRef.current.id); - onSave(); - onClose(); + console.log("Attempting to delete format with ID:", initialFormat.id); + const response = await deleteFormat(initialFormat.id); + console.log("Delete response:", response); + if (response.error) { + Alert.error(`Cannot delete: ${response.message}`); + } else { + Alert.success("Format deleted successfully"); + onSave(); + onClose(); + } } catch (error) { - console.error('Error deleting format:', error); - setError('Failed to delete format. Please try again.'); + console.error("Error deleting format:", error); + Alert.error("Failed to delete format. Please try again."); + } finally { + setIsDeleting(false); } + } else { + setIsDeleting(true); } }; @@ -89,7 +104,9 @@ function FormatModal({ format = null, isOpen, onClose, onSave }) { const handleSaveCondition = (newCondition) => { if (selectedCondition) { - setConditions(conditions.map(c => c === selectedCondition ? newCondition : c)); + setConditions( + conditions.map((c) => (c === selectedCondition ? newCondition : c)) + ); } else { setConditions([...conditions, newCondition]); } @@ -97,122 +114,134 @@ function FormatModal({ format = null, isOpen, onClose, onSave }) { }; const handleDeleteCondition = (conditionToDelete) => { - setConditions(conditions.filter(c => c !== conditionToDelete)); + setConditions(conditions.filter((c) => c !== conditionToDelete)); }; const handleAddTag = () => { if (newTag.trim() && !tags.includes(newTag.trim())) { setTags([...tags, newTag.trim()]); - setNewTag(''); + setNewTag(""); } }; const handleRemoveTag = (tagToRemove) => { - setTags(tags.filter(tag => tag !== tagToRemove)); + setTags(tags.filter((tag) => tag !== tagToRemove)); }; return ( - - {error &&
{error}
} -
- - setName(e.target.value)} - placeholder="Enter format name" - className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" - /> -
-
- - setDescription(e.target.value)} - placeholder="Enter description" - className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" - /> -
-
- -
- {tags.map(tag => ( - - {tag} - - - ))} -
-
+ <> + + {error &&
{error}
} +
+ setNewTag(e.target.value)} - placeholder="Add a tag" - className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Enter format name" + className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" /> -
-
-

Conditions:

-
- {conditions.map((condition, index) => ( - handleOpenConditionModal(condition)} +
+ + setDescription(e.target.value)} + placeholder="Enter description" + className="w-full p-3 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" /> - ))} -
- -
+
+
+ +
+ {tags.map((tag) => ( + + {tag} + + + ))} +
+
+ setNewTag(e.target.value)} + placeholder="Add a tag" + className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + /> + +
+
+

Conditions:

+
+ {conditions.map((condition, index) => ( + handleOpenConditionModal(condition)} + /> + ))} +
- {format && ( +
- )} -
- - + {initialFormat && ( + + )} +
+ + + ); } @@ -235,4 +264,4 @@ FormatModal.propTypes = { onSave: PropTypes.func.isRequired, }; -export default FormatModal; \ No newline at end of file +export default FormatModal; diff --git a/frontend/src/components/profile/ProfileModal.jsx b/frontend/src/components/profile/ProfileModal.jsx index efa4647..9a43648 100644 --- a/frontend/src/components/profile/ProfileModal.jsx +++ b/frontend/src/components/profile/ProfileModal.jsx @@ -1,37 +1,61 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { saveProfile, deleteProfile } from "../../api/api"; +import { saveProfile, deleteProfile, getFormats } from "../../api/api"; import Modal from "../ui/Modal"; +import Alert from "../ui/Alert"; function unsanitize(text) { return text.replace(/\\:/g, ":").replace(/\\n/g, "\n"); } -function ProfileModal({ profile = null, isOpen, onClose, onSave }) { +function ProfileModal({ profile: initialProfile, isOpen, onClose, onSave }) { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [tags, setTags] = useState([]); const [newTag, setNewTag] = useState(""); + const [customFormats, setCustomFormats] = useState([]); const [error, setError] = useState(""); - const initialProfileRef = useRef(profile); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { if (isOpen) { - if (profile && profile.id !== 0) { - initialProfileRef.current = profile; - setName(unsanitize(profile.name)); - setDescription(unsanitize(profile.description)); - setTags(profile.tags ? profile.tags.map(unsanitize) : []); + if (initialProfile && initialProfile.id !== 0) { + setName(unsanitize(initialProfile.name)); + setDescription(unsanitize(initialProfile.description)); + setTags(initialProfile.tags ? initialProfile.tags.map(unsanitize) : []); + setCustomFormats(initialProfile.custom_formats || []); } else { - initialProfileRef.current = null; - setName(profile ? unsanitize(profile.name) : ""); - setDescription(profile ? unsanitize(profile.description) : ""); - setTags(profile ? profile.tags.map(unsanitize) : []); + setName(""); + setDescription(""); + setTags([]); + setCustomFormats([]); } setError(""); setNewTag(""); + fetchAllFormats(); } - }, [profile, isOpen]); + }, [initialProfile, isOpen]); + + const fetchAllFormats = async () => { + try { + const formats = await getFormats(); + const updatedCustomFormats = formats.map((format) => { + const existingFormat = + initialProfile && initialProfile.custom_formats + ? initialProfile.custom_formats.find((cf) => cf.id === format.id) + : null; + return { + id: format.id, + name: format.name, + score: existingFormat ? existingFormat.score : 0, + }; + }); + setCustomFormats(updatedCustomFormats); + } catch (error) { + console.error("Error fetching formats:", error); + setError("Failed to fetch formats."); + } + }; const handleSave = async () => { if (!name.trim()) { @@ -40,10 +64,11 @@ function ProfileModal({ profile = null, isOpen, onClose, onSave }) { } try { await saveProfile({ - id: profile ? profile.id : 0, + id: initialProfile ? initialProfile.id : 0, name, description, tags, + custom_formats: customFormats, }); onSave(); onClose(); @@ -54,18 +79,26 @@ function ProfileModal({ profile = null, isOpen, onClose, onSave }) { }; const handleDelete = async () => { - const confirmDeletion = window.confirm( - "Are you sure you want to delete this profile?" - ); - if (!confirmDeletion) return; - - try { - await deleteProfile(profile.id); - onSave(); - onClose(); - } catch (error) { - console.error("Error deleting profile:", error); - setError("Failed to delete profile. Please try again."); + if (isDeleting) { + try { + console.log("Attempting to delete profile with ID:", initialProfile.id); + const response = await deleteProfile(initialProfile.id); + console.log("Delete response:", response); + if (response.error) { + Alert.error(`Failed to delete profile: ${response.message}`); + } else { + Alert.success("Profile deleted successfully"); + onSave(); + onClose(); + } + } catch (error) { + console.error("Error deleting profile:", error); + Alert.error("Failed to delete profile. Please try again."); + } finally { + setIsDeleting(false); + } + } else { + setIsDeleting(true); } }; @@ -80,11 +113,21 @@ function ProfileModal({ profile = null, isOpen, onClose, onSave }) { setTags(tags.filter((tag) => tag !== tagToRemove)); }; + const handleScoreChange = (formatId, score) => { + setCustomFormats( + customFormats.map((format) => + format.id === formatId + ? { ...format, score: Math.max(parseInt(score) || 0, 0) } + : format + ) + ); + }; + return ( {error &&
{error}
}
@@ -147,13 +190,36 @@ function ProfileModal({ profile = null, isOpen, onClose, onSave }) {
+
+ +
+ {customFormats.map((format) => ( +
+ {format.name} + handleScoreChange(format.id, e.target.value)} + className="w-20 p-1 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + min="0" + /> +
+ ))} +
+
- + {initialProfile && ( + + )} -
- ) : ( - - )} - -
- - setDescription(e.target.value)} - placeholder="Enter description" - className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" - /> -
-
- -
- {tags.map(tag => ( - - {tag} - - - ))} -
-
+ <> + + {error &&
{error}
} +
+ setNewTag(e.target.value)} - placeholder="Add a tag" - className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + value={name} + onChange={(e) => setName(e.target.value)} + placeholder="Enter regex name" + className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" /> +
+
+ + setPattern(e.target.value)} + placeholder="Enter regex pattern" + className="w-full p-2 border rounded font-mono dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + /> +
+
+ {regex101Link ? ( +
+ + View in Regex101 + + +
+ ) : ( + + )} +
+
+ + setDescription(e.target.value)} + placeholder="Enter description" + className="w-full p-2 border rounded dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + /> +
+
+ +
+ {tags.map((tag) => ( + + {tag} + + + ))} +
+
+ setNewTag(e.target.value)} + placeholder="Add a tag" + className="flex-grow p-2 border rounded-l dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600" + /> + +
+
+
+ {initialRegex && ( + + )}
-
-
- - -
- + + ); } @@ -310,4 +328,4 @@ RegexModal.propTypes = { onSave: PropTypes.func.isRequired, }; -export default RegexModal; \ No newline at end of file +export default RegexModal;