feat(delete): Improve delete functionality - Don't allow deletion of regex / format in use - Add confirmation for deleting before allowing request

This commit is contained in:
santiagosayshey
2024-08-24 02:06:17 +09:30
committed by Sam Chau
parent 8cb3d7a827
commit 174e4ec82d
8 changed files with 582 additions and 412 deletions

View File

@@ -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

View File

@@ -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."}

View File

@@ -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):

View File

@@ -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."}

View File

@@ -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);

View File

@@ -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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialFormatRef.current ? 'Edit Custom Format' : 'Add Custom Format'}
className="max-w-3xl min-h-96"
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Format Name
</label>
<input
type="text"
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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<input
type="text"
value={description}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map(tag => (
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
{tag}
<button onClick={() => handleRemoveTag(tag)} className="ml-1 text-xs">&times;</button>
</span>
))}
</div>
<div className="flex">
<>
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialFormat ? "Edit Custom Format" : "Add Custom Format"}
className="max-w-3xl min-h-96"
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Format Name
</label>
<input
type="text"
value={newTag}
onChange={(e) => 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"
/>
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
>
Add
</button>
</div>
</div>
<h3 className="font-bold mb-4 dark:text-gray-300">Conditions:</h3>
<div className="mb-6 max-h-96 overflow-y-auto grid grid-cols-1 gap-4">
{conditions.map((condition, index) => (
<ConditionCard
key={index}
condition={condition}
onEdit={() => handleOpenConditionModal(condition)}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<input
type="text"
value={description}
onChange={(e) => 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"
/>
))}
</div>
<button
onClick={() => handleOpenConditionModal()}
className="bg-green-500 text-white px-4 py-3 rounded hover:bg-green-600 mb-6 transition-colors"
>
Add Condition
</button>
<div className="flex justify-between">
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map((tag) => (
<span
key={tag}
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="ml-1 text-xs"
>
&times;
</button>
</span>
))}
</div>
<div className="flex">
<input
type="text"
value={newTag}
onChange={(e) => 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"
/>
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
>
Add
</button>
</div>
</div>
<h3 className="font-bold mb-4 dark:text-gray-300">Conditions:</h3>
<div className="mb-6 max-h-96 overflow-y-auto grid grid-cols-1 gap-4">
{conditions.map((condition, index) => (
<ConditionCard
key={index}
condition={condition}
onEdit={() => handleOpenConditionModal(condition)}
/>
))}
</div>
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600 transition-colors"
onClick={() => handleOpenConditionModal()}
className="bg-green-500 text-white px-4 py-3 rounded hover:bg-green-600 mb-6 transition-colors"
>
Save
Add Condition
</button>
{format && (
<div className="flex justify-between">
<button
onClick={handleDelete}
className="bg-red-500 text-white px-4 py-3 rounded hover:bg-red-600 transition-colors"
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-3 rounded hover:bg-blue-600 transition-colors"
>
Delete
Save
</button>
)}
</div>
<ConditionModal
condition={selectedCondition}
isOpen={isConditionModalOpen}
onClose={handleCloseConditionModal}
onSave={handleSaveCondition}
onDelete={handleDeleteCondition}
regexes={regexes}
level={1}
/>
</Modal>
{initialFormat && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-3 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
{isDeleting ? "Confirm Delete" : "Delete"}
</button>
)}
</div>
<ConditionModal
condition={selectedCondition}
isOpen={isConditionModalOpen}
onClose={handleCloseConditionModal}
onSave={handleSaveCondition}
onDelete={handleDeleteCondition}
regexes={regexes}
level={1}
/>
</Modal>
</>
);
}
@@ -235,4 +264,4 @@ FormatModal.propTypes = {
onSave: PropTypes.func.isRequired,
};
export default FormatModal;
export default FormatModal;

View File

@@ -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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialProfileRef.current ? "Edit Profile" : "Add Profile"}
title={initialProfile ? "Edit Profile" : "Add Profile"}
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
@@ -147,13 +190,36 @@ function ProfileModal({ profile = null, isOpen, onClose, onSave }) {
</button>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Custom Formats
</label>
<div className="max-h-60 overflow-y-auto">
{customFormats.map((format) => (
<div key={format.id} className="flex items-center space-x-2 mb-2">
<span className="flex-grow">{format.name}</span>
<input
type="number"
value={format.score}
onChange={(e) => 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"
/>
</div>
))}
</div>
</div>
<div className="flex justify-between">
<button
onClick={handleDelete}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
>
Delete
</button>
{initialProfile && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
{isDeleting ? "Confirm Delete" : "Delete"}
</button>
)}
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
@@ -171,6 +237,13 @@ ProfileModal.propTypes = {
name: PropTypes.string.isRequired,
description: PropTypes.string,
tags: PropTypes.arrayOf(PropTypes.string),
custom_formats: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
score: PropTypes.number.isRequired,
})
),
}),
isOpen: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,

View File

@@ -1,107 +1,109 @@
import { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { saveRegex, deleteRegex, createRegex101Link } from '../../api/api';
import Modal from '../ui/Modal';
import { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import { saveRegex, deleteRegex, createRegex101Link } from "../../api/api";
import Modal from "../ui/Modal";
import Alert from "../ui/Alert";
function unsanitize(text) {
return text.replace(/\\:/g, ':').replace(/\\n/g, '\n');
return text.replace(/\\:/g, ":").replace(/\\n/g, "\n");
}
function RegexModal({ regex = null, isOpen, onClose, onSave }) {
const [name, setName] = useState('');
const [pattern, setPattern] = useState('');
const [description, setDescription] = useState('');
function RegexModal({ regex: initialRegex, isOpen, onClose, onSave }) {
const [name, setName] = useState("");
const [pattern, setPattern] = useState("");
const [description, setDescription] = useState("");
const [tags, setTags] = useState([]);
const [newTag, setNewTag] = useState('');
const [regex101Link, setRegex101Link] = useState('');
const [error, setError] = useState('');
const [newTag, setNewTag] = useState("");
const [regex101Link, setRegex101Link] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const initialRegexRef = useRef(regex);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
if (isOpen) {
if (regex && regex.id !== 0) {
initialRegexRef.current = regex;
setName(unsanitize(regex.name));
setPattern(regex.pattern);
setDescription(unsanitize(regex.description));
setTags(regex.tags ? regex.tags.map(unsanitize) : []);
setRegex101Link(regex.regex101Link || '');
if (initialRegex && initialRegex.id !== 0) {
setName(unsanitize(initialRegex.name));
setPattern(initialRegex.pattern);
setDescription(unsanitize(initialRegex.description));
setTags(initialRegex.tags ? initialRegex.tags.map(unsanitize) : []);
setRegex101Link(initialRegex.regex101Link || "");
} else {
initialRegexRef.current = null;
setName(regex ? unsanitize(regex.name) : '');
setPattern(regex ? regex.pattern : '');
setDescription(regex ? unsanitize(regex.description) : '');
setTags(regex ? regex.tags.map(unsanitize) : []);
setRegex101Link('');
setName("");
setPattern("");
setDescription("");
setTags([]);
setRegex101Link("");
}
setError('');
setNewTag('');
setError("");
setNewTag("");
setIsLoading(false);
setIsDeleting(false);
}
}, [regex, isOpen]);
}, [initialRegex, isOpen]);
const handleCreateRegex101Link = async () => {
if (!pattern.trim()) {
setError('Please provide a regex pattern before creating tests.');
setError("Please provide a regex pattern before creating tests.");
return;
}
const unitTests = [
{
description: "Test if 'D-Z0N3' is detected correctly",
testString: "Test D-Z0N3 pattern",
criteria: "DOES_MATCH",
target: "REGEX"
},
{
description: "Test if 'random text' does not match",
testString: "random text",
criteria: "DOES_NOT_MATCH",
target: "REGEX"
}
{
description: "Test if 'D-Z0N3' is detected correctly",
testString: "Test D-Z0N3 pattern",
criteria: "DOES_MATCH",
target: "REGEX",
},
{
description: "Test if 'random text' does not match",
testString: "random text",
criteria: "DOES_NOT_MATCH",
target: "REGEX",
},
];
setIsLoading(true);
try {
const response = await createRegex101Link({
regex: pattern,
flavor: 'pcre',
flags: 'gmi',
delimiter: '/',
unitTests: unitTests
});
const permalinkFragment = response.permalinkFragment;
const response = await createRegex101Link({
regex: pattern,
flavor: "pcre",
flags: "gmi",
delimiter: "/",
unitTests: unitTests,
});
const permalinkFragment = response.permalinkFragment;
const regex101Link = `https://regex101.com/r/${permalinkFragment}`;
setRegex101Link(regex101Link);
const regex101Link = `https://regex101.com/r/${permalinkFragment}`;
setRegex101Link(regex101Link);
await saveRegex({
id: regex ? regex.id : 0,
name,
pattern,
description,
tags,
regex101Link,
});
await saveRegex({
id: regex ? regex.id : 0,
name,
pattern,
description,
tags,
regex101Link,
});
window.open(regex101Link, '_blank');
onSave();
setError('');
window.open(regex101Link, "_blank");
onSave();
setError("");
} catch (error) {
console.error('Error creating regex101 link:', error);
setError('Failed to create regex101 link. Please try again.');
console.error("Error creating regex101 link:", error);
setError("Failed to create regex101 link. Please try again.");
} finally {
setIsLoading(false);
setIsLoading(false);
}
};
const handleRemoveRegex101Link = async () => {
const confirmRemoval = window.confirm("Are you sure you want to remove this Regex101 link?");
const confirmRemoval = window.confirm(
"Are you sure you want to remove this Regex101 link?"
);
if (!confirmRemoval) return;
setIsLoading(true);
setRegex101Link('');
setRegex101Link("");
try {
await saveRegex({
@@ -110,189 +112,205 @@ function RegexModal({ regex = null, isOpen, onClose, onSave }) {
pattern,
description,
tags,
regex101Link: '',
regex101Link: "",
});
onSave();
setError('');
setError("");
} catch (error) {
console.error('Error removing regex101 link:', error);
setError('Failed to remove regex101 link. Please try again.');
console.error("Error removing regex101 link:", error);
setError("Failed to remove regex101 link. Please try again.");
} finally {
setIsLoading(false);
setIsLoading(false);
}
};
const handleSave = async () => {
if (!name.trim() || !pattern.trim()) {
setError('Name and pattern are required.');
setError("Name and pattern are required.");
return;
}
try {
await saveRegex({
id: regex ? regex.id : 0,
name,
pattern,
description,
await saveRegex({
id: initialRegex ? initialRegex.id : 0,
name,
pattern,
description,
tags,
regex101Link
regex101Link,
});
onSave();
onClose();
} catch (error) {
console.error('Error saving regex:', error);
setError('Failed to save regex. Please try again.');
console.error("Error saving regex:", error);
setError("Failed to save regex. Please try again.");
}
};
const handleDelete = async () => {
const confirmDeletion = window.confirm("Are you sure you want to delete this regex?");
if (!confirmDeletion) return;
try {
const response = await deleteRegex(regex.id);
if (isDeleting) {
try {
console.log("Attempting to delete regex with ID:", initialRegex.id);
const response = await deleteRegex(initialRegex.id);
console.log("Delete response:", response);
if (response.error) {
if (response.error === 'Regex in use') {
setError('This regex is being used in one or more custom formats. Please remove it from those formats before deleting.');
} else {
setError('Failed to delete regex. Please try again.');
}
Alert.error(`Cannot delete: ${response.message}`);
} else {
onSave();
onClose();
Alert.success("Regex deleted successfully");
onSave();
onClose();
}
} catch (error) {
console.error('Error deleting regex:', error);
setError('Failed to delete regex. Please try again.');
} catch (error) {
console.error("Error deleting regex:", error);
Alert.error("Failed to delete regex. Please try again.");
} finally {
setIsDeleting(false);
}
} else {
setIsDeleting(true);
}
};
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 (
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialRegexRef.current ? 'Edit Regex Pattern' : 'Add Regex Pattern'}
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Regex Name
</label>
<input
type="text"
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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Regex Pattern
</label>
<input
type="text"
value={pattern}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
{regex101Link ? (
<div className="flex items-center space-x-2">
<a
href={regex101Link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 transition-colors"
>
View in Regex101
</a>
<button
onClick={handleRemoveRegex101Link}
className="text-red-500 hover:text-red-600 transition-colors"
disabled={isLoading}
>
{isLoading ? 'Removing...' : 'Remove Link'}
</button>
</div>
) : (
<button
onClick={handleCreateRegex101Link}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition-colors"
disabled={isLoading}
>
{isLoading ? 'Creating Tests...' : 'Create Tests in Regex101'}
</button>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description (Optional)
</label>
<input
type="text"
value={description}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map(tag => (
<span key={tag} className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
{tag}
<button onClick={() => handleRemoveTag(tag)} className="ml-1 text-xs">&times;</button>
</span>
))}
</div>
<div className="flex">
<>
<Modal
isOpen={isOpen}
onClose={onClose}
title={initialRegex ? "Edit Regex Pattern" : "Add Regex Pattern"}
>
{error && <div className="text-red-500 mb-4">{error}</div>}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Regex Name
</label>
<input
type="text"
value={newTag}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Regex Pattern
</label>
<input
type="text"
value={pattern}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
{regex101Link ? (
<div className="flex items-center space-x-2">
<a
href={regex101Link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:text-blue-600 transition-colors"
>
View in Regex101
</a>
<button
onClick={handleRemoveRegex101Link}
className="text-red-500 hover:text-red-600 transition-colors"
disabled={isLoading}
>
{isLoading ? "Removing..." : "Remove Link"}
</button>
</div>
) : (
<button
onClick={handleCreateRegex101Link}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 transition-colors"
disabled={isLoading}
>
{isLoading ? "Creating Tests..." : "Create Tests in Regex101"}
</button>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description (Optional)
</label>
<input
type="text"
value={description}
onChange={(e) => 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"
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tags
</label>
<div className="flex flex-wrap mb-2">
{tags.map((tag) => (
<span
key={tag}
className="bg-blue-100 text-blue-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300"
>
{tag}
<button
onClick={() => handleRemoveTag(tag)}
className="ml-1 text-xs"
>
&times;
</button>
</span>
))}
</div>
<div className="flex">
<input
type="text"
value={newTag}
onChange={(e) => 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"
/>
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
>
Add
</button>
</div>
</div>
<div className="flex justify-between">
{initialRegex && (
<button
onClick={handleDelete}
className={`bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors ${
isDeleting ? "bg-red-600" : ""
}`}
>
{isDeleting ? "Confirm Delete" : "Delete"}
</button>
)}
<button
onClick={handleAddTag}
className="bg-blue-500 text-white px-4 py-2 rounded-r hover:bg-blue-600 transition-colors"
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Add
Save
</button>
</div>
</div>
<div className="flex justify-between">
<button
onClick={handleDelete}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 transition-colors"
>
Delete
</button>
<button
onClick={handleSave}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 transition-colors"
>
Save
</button>
</div>
</Modal>
</Modal>
</>
);
}
@@ -310,4 +328,4 @@ RegexModal.propTypes = {
onSave: PropTypes.func.isRequired,
};
export default RegexModal;
export default RegexModal;