mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
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:
committed by
Sam Chau
parent
8cb3d7a827
commit
174e4ec82d
24
backend/app/data_operations.py
Normal file
24
backend/app/data_operations.py
Normal 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
|
||||
@@ -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."}
|
||||
@@ -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):
|
||||
|
||||
@@ -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."}
|
||||
@@ -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);
|
||||
|
||||
@@ -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">×</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"
|
||||
>
|
||||
×
|
||||
</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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">×</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"
|
||||
>
|
||||
×
|
||||
</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;
|
||||
|
||||
Reference in New Issue
Block a user