-
-
-
@@ -693,7 +820,7 @@
Ready
- Tools.yaml Editor v1.0 |
+ Tools.yaml Editor v1.1 |
Shortcuts: Ctrl+S (Save), Ctrl+N (New), Ctrl+E (Export), Alt+V (Validate), Ctrl+F (Search), Esc (Clear)
@@ -712,16 +839,18 @@
let filteredTools = [];
let selectedTool = null;
let editingIndex = -1;
+ let selectedCategory = null;
+ let editingCategoryIndex = -1;
+ let currentSection = 'tools';
// Initialize the application
function init() {
- // Load theme preference
const savedTheme = localStorage.getItem('tools-editor-theme') || 'dark';
document.body.setAttribute('data-theme', savedTheme);
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.textContent = savedTheme === 'light' ? 'š' : 'āļø';
-
- // Load sample domains and phases if none exist
+
+ // Load sample data if none exists
if (toolsData.domains.length === 0) {
toolsData.domains = [
{ id: 'incident-response', name: 'Incident Response' },
@@ -732,7 +861,7 @@
{ id: 'cloud-forensics', name: 'Cloud Forensics' }
];
}
-
+
if (toolsData.phases.length === 0) {
toolsData.phases = [
{ id: 'data-collection', name: 'Data Collection' },
@@ -742,11 +871,38 @@
];
}
+ if (toolsData['domain-agnostic-software'].length === 0) {
+ toolsData['domain-agnostic-software'] = [
+ { id: 'data-recovery', name: 'Data Recovery', description: 'Tools for recovering deleted or corrupted data' },
+ { id: 'file-analysis', name: 'File Analysis', description: 'Tools for analyzing file contents and metadata' },
+ { id: 'documentation', name: 'Documentation', description: 'Tools for creating reports and documentation' }
+ ];
+ }
+
populateFilters();
+
+ // ā
Initialize filteredTools to everything at startup
+ filteredTools = Array.isArray(toolsData.tools) ? toolsData.tools.slice() : [];
renderTools();
+ renderCategories();
updateStatus('Application initialized');
}
+
+ // Section switching
+ function switchSection(section) {
+ currentSection = section;
+ document.querySelectorAll('.section-tab').forEach(tab => tab.classList.remove('active'));
+ document.querySelectorAll('.section-content').forEach(content => content.classList.remove('active'));
+
+ document.querySelector(`[onclick="switchSection('${section}')"]`).classList.add('active');
+ document.getElementById(`${section}-section`).classList.add('active');
+
+ if (section === 'categories') {
+ renderCategories();
+ }
+ }
+
// Theme management
function toggleTheme() {
const body = document.body;
@@ -754,7 +910,6 @@
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
body.setAttribute('data-theme', newTheme);
- // Save theme preference
localStorage.setItem('tools-editor-theme', newTheme);
const themeToggle = document.querySelector('.theme-toggle');
@@ -800,6 +955,8 @@
toolsData.phases.forEach(phase => values.add(phase.id));
} else if (field === 'platforms') {
['Windows', 'macOS', 'Linux', 'Web', 'Mobile', 'Cross-platform'].forEach(p => values.add(p));
+ } else if (field === 'domain-agnostic-software') {
+ toolsData['domain-agnostic-software'].forEach(cat => values.add(cat.id));
} else if (field === 'related_concepts') {
toolsData.tools.filter(t => t.type === 'concept').forEach(t => values.add(t.name));
} else if (field === 'related_software') {
@@ -809,7 +966,7 @@
return Array.from(values).sort();
}
- // Tag input handling
+ // Tag input handling - consolidated function
function focusTagInput(containerId) {
const input = document.querySelector(`#${containerId} input`);
input.focus();
@@ -817,7 +974,7 @@
function handleTagInput(event, field) {
const dropdown = document.getElementById(`${field}Autocomplete`);
- const items = dropdown.querySelectorAll('.autocomplete-item');
+ const items = dropdown ? dropdown.querySelectorAll('.autocomplete-item') : [];
let selectedIndex = Array.from(items).findIndex(item => item.classList.contains('selected'));
if (event.key === 'ArrowDown') {
@@ -868,10 +1025,7 @@
const input = container.querySelector('input');
// Check if tag already exists
- const existingTags = Array.from(container.querySelectorAll('.tag-input-item')).map(item =>
- item.textContent.replace('Ć', '').trim()
- );
-
+ const existingTags = getTagValues(field);
if (existingTags.includes(value)) return;
const tagElement = document.createElement('div');
@@ -895,6 +1049,7 @@
function getTagValues(field) {
const container = document.getElementById(`${field}Input`);
+ if (!container) return [];
return Array.from(container.querySelectorAll('.tag-input-item')).map(item =>
item.textContent.replace('Ć', '').trim()
);
@@ -902,6 +1057,7 @@
function setTagValues(field, values) {
const container = document.getElementById(`${field}Input`);
+ if (!container) return;
// Clear existing tags
container.querySelectorAll('.tag-input-item').forEach(item => item.remove());
// Add new tags
@@ -916,7 +1072,7 @@
const value = input.value.toLowerCase();
const dropdown = document.getElementById(`${field}Autocomplete`);
- if (value.length < 1) {
+ if (!dropdown || value.length < 1) {
hideAutocomplete(field);
return;
}
@@ -934,7 +1090,7 @@
suggestions.slice(0, 10).forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
- if (index === 0) item.classList.add('selected'); // Select first item by default
+ if (index === 0) item.classList.add('selected');
item.textContent = suggestion;
item.onclick = () => {
addTag(field, suggestion);
@@ -948,11 +1104,99 @@
}
function hideAutocomplete(field) {
- document.getElementById(`${field}Autocomplete`).style.display = 'none';
+ const dropdown = document.getElementById(`${field}Autocomplete`);
+ if (dropdown) dropdown.style.display = 'none';
+ }
+
+ // Consolidated validation function
+ function validateTool(tool, index = null) {
+ const prefix = index !== null ? `Tool ${index + 1} (${tool.name || 'Unnamed'}): ` : '';
+ const errors = [];
+
+ // Required fields validation
+ if (!tool.name || typeof tool.name !== 'string' || tool.name.trim() === '') {
+ errors.push(prefix + 'Name is required and must be a non-empty string');
+ }
+
+ if (!tool.type || !['software', 'method', 'concept'].includes(tool.type)) {
+ errors.push(prefix + 'Type is required and must be one of: software, method, concept');
+ }
+
+ if (!tool.description || typeof tool.description !== 'string' || tool.description.trim() === '') {
+ errors.push(prefix + 'Description is required and must be a non-empty string');
+ }
+
+ if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
+ errors.push(prefix + 'URL is required and must be a non-empty string');
+ }
+
+ if (!tool.skillLevel || !['novice', 'beginner', 'intermediate', 'advanced', 'expert'].includes(tool.skillLevel)) {
+ errors.push(prefix + 'Skill level is required and must be one of: novice, beginner, intermediate, advanced, expert');
+ }
+
+ // URL format validation
+ if (tool.url && tool.url.trim() !== '') {
+ try {
+ new URL(tool.url);
+ } catch {
+ errors.push(prefix + 'URL must be a valid URL format');
+ }
+ }
+
+ // Project URL validation (if provided)
+ if (tool.projectUrl && tool.projectUrl.trim() !== '') {
+ try {
+ new URL(tool.projectUrl);
+ } catch {
+ errors.push(prefix + 'Project URL must be a valid URL format');
+ }
+ }
+
+ // Status URL validation (if provided)
+ if (tool.statusUrl && tool.statusUrl.trim() !== '') {
+ try {
+ new URL(tool.statusUrl);
+ } catch {
+ errors.push(prefix + 'Status URL must be a valid URL format');
+ }
+ }
+
+ // Array fields validation
+ const arrayFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software', 'domain-agnostic-software'];
+ arrayFields.forEach(field => {
+ if (tool[field] !== undefined && tool[field] !== null && !Array.isArray(tool[field])) {
+ errors.push(prefix + `${field} must be an array if provided`);
+ }
+ });
+
+ // String fields validation (optional but must be strings if provided)
+ const stringFields = ['icon', 'license', 'accessType', 'statusUrl'];
+ stringFields.forEach(field => {
+ if (tool[field] !== undefined && tool[field] !== null && typeof tool[field] !== 'string') {
+ errors.push(prefix + `${field} must be a string if provided`);
+ }
+ });
+
+ // Boolean field validation
+ if (tool.knowledgebase !== undefined && tool.knowledgebase !== null && typeof tool.knowledgebase !== 'boolean') {
+ errors.push(prefix + 'knowledgebase must be a boolean if provided');
+ }
+
+ // Duplicate name check: only when we know which index we're validating
+ if (index !== null && tool.name) {
+ const clashIndex = toolsData.tools.findIndex((t, i) => i !== index && t.name === tool.name);
+ if (clashIndex >= 0) {
+ errors.push(prefix + `Duplicate name "${tool.name}" also found at index ${clashIndex}`);
+ }
+ }
+
+
+ return errors;
}
// Tool management
function addNewTool() {
+ switchSection('tools');
clearForm();
editingIndex = -1;
document.getElementById('editTitle').textContent = 'Add New Tool';
@@ -961,28 +1205,31 @@
}
function editTool(index) {
+ switchSection('tools');
const tool = filteredTools[index];
editingIndex = toolsData.tools.indexOf(tool);
- // Populate form
+ // Populate form with all fields
document.getElementById('name').value = tool.name || '';
document.getElementById('type').value = tool.type || '';
document.getElementById('icon').value = tool.icon || '';
document.getElementById('description').value = tool.description || '';
document.getElementById('url').value = tool.url || '';
document.getElementById('projectUrl').value = tool.projectUrl || '';
+ document.getElementById('statusUrl').value = tool.statusUrl || '';
document.getElementById('skillLevel').value = tool.skillLevel || '';
document.getElementById('license').value = tool.license || '';
document.getElementById('accessType').value = tool.accessType || '';
document.getElementById('knowledgebase').checked = tool.knowledgebase || false;
- // Set tag values
+ // Set tag values for all tag fields
setTagValues('domains', tool.domains);
setTagValues('phases', tool.phases);
setTagValues('platforms', tool.platforms);
setTagValues('tags', tool.tags);
setTagValues('related_concepts', tool.related_concepts);
setTagValues('related_software', tool.related_software);
+ setTagValues('domain-agnostic-software', tool['domain-agnostic-software']);
document.getElementById('editTitle').textContent = `Edit: ${tool.name}`;
document.getElementById('deleteBtn').style.display = 'inline-block';
@@ -1011,18 +1258,18 @@
related_software: getTagValues('related_software')
};
+ // Add domain-agnostic-software field
+ const domainAgnosticCategories = getTagValues('domain-agnostic-software');
+ if (domainAgnosticCategories.length > 0) {
+ tool['domain-agnostic-software'] = domainAgnosticCategories;
+ }
+
// Add optional fields only if they have values
- const icon = document.getElementById('icon').value.trim();
- if (icon) tool.icon = icon;
-
- const projectUrl = document.getElementById('projectUrl').value.trim();
- if (projectUrl) tool.projectUrl = projectUrl;
-
- const license = document.getElementById('license').value.trim();
- if (license) tool.license = license;
-
- const accessType = document.getElementById('accessType').value;
- if (accessType) tool.accessType = accessType;
+ const optionalFields = ['icon', 'projectUrl', 'statusUrl', 'license', 'accessType'];
+ optionalFields.forEach(field => {
+ const value = document.getElementById(field).value.trim();
+ if (value) tool[field] = value;
+ });
if (document.getElementById('knowledgebase').checked) {
tool.knowledgebase = true;
@@ -1048,37 +1295,22 @@
const toolName = tool.name;
if (editingIndex >= 0) {
- // Update existing tool
toolsData.tools[editingIndex] = tool;
-
- // Show immediate feedback
updateStatus(`ā
Updated ${toolName}`);
-
- // Add temporary success indicator to the form
showSaveSuccess('Tool updated successfully!');
-
} else {
- // Add new tool
toolsData.tools.push(tool);
-
- // Show immediate feedback
updateStatus(`ā
Added ${toolName}`);
-
- // Add temporary success indicator to the form
showSaveSuccess('Tool added successfully!');
}
- // Refresh the view immediately - this will update filtered results
- filterTools(); // This ensures tools disappear from "invalid" filter if fixed
-
- // Clear form after successful save
+ filterTools();
clearForm();
- // If we were viewing validation errors, update the validation summary
+ // Update validation summary if needed
setTimeout(() => {
const validationFilter = document.getElementById('validationFilter').value;
if (validationFilter === 'invalid') {
- // Re-run validation check to update counts
const remainingErrors = [];
toolsData.tools.forEach((t, index) => {
const toolErrors = validateTool(t, index);
@@ -1086,19 +1318,15 @@
});
if (remainingErrors.length === 0) {
- // No more invalid tools - show success message
hideValidationErrors();
updateStatus('š All validation errors fixed!');
-
- // Optionally clear the validation filter
document.getElementById('validationFilter').value = '';
filterTools();
} else {
- // Update validation summary
const validationSummary = document.getElementById('validationSummary');
const validationCount = document.getElementById('validationCount');
if (validationSummary && validationCount) {
- validationCount.textContent = toolsData.tools.filter(t => validateToolBasic(t).length > 0).length;
+ validationCount.textContent = toolsData.tools.filter(t => validateTool(t, toolsData.tools.indexOf(t)).length > 0).length;
validationSummary.style.display = validationCount.textContent === '0' ? 'none' : 'block';
}
}
@@ -1106,9 +1334,7 @@
}, 100);
}
- // Helper function to show save success feedback
function showSaveSuccess(message) {
- // Create or update success indicator
let successDiv = document.getElementById('saveSuccess');
if (!successDiv) {
successDiv = document.createElement('div');
@@ -1132,13 +1358,11 @@
successDiv.textContent = message;
- // Animate in
requestAnimationFrame(() => {
successDiv.style.opacity = '1';
successDiv.style.transform = 'translateY(0)';
});
- // Animate out after 3 seconds
setTimeout(() => {
successDiv.style.opacity = '0';
successDiv.style.transform = 'translateY(-20px)';
@@ -1163,8 +1387,8 @@
function clearForm() {
document.getElementById('toolForm').reset();
- // Clear tag inputs
- ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software'].forEach(field => {
+ const tagFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software', 'domain-agnostic-software'];
+ tagFields.forEach(field => {
setTagValues(field, []);
});
editingIndex = -1;
@@ -1172,121 +1396,93 @@
document.getElementById('deleteBtn').style.display = 'none';
}
- // Basic validation for rendering (no duplicate checks)
- function validateToolBasic(tool) {
- const errors = [];
+ // Category management
+ function renderCategories() {
+ const container = document.getElementById('categoriesList');
+ if (!container) return;
- // Required fields validation
- if (!tool.name || typeof tool.name !== 'string' || tool.name.trim() === '') {
- errors.push('Name is required and must be a non-empty string');
- }
-
- if (!tool.type || !['software', 'method', 'concept'].includes(tool.type)) {
- errors.push('Type is required and must be one of: software, method, concept');
- }
-
- if (!tool.description || typeof tool.description !== 'string' || tool.description.trim() === '') {
- errors.push('Description is required and must be a non-empty string');
- }
-
- if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
- errors.push('URL is required and must be a non-empty string');
- }
-
- if (!tool.skillLevel || !['novice', 'beginner', 'intermediate', 'advanced', 'expert'].includes(tool.skillLevel)) {
- errors.push('Skill level is required and must be one of: novice, beginner, intermediate, advanced, expert');
- }
-
- // URL format validation
- if (tool.url && tool.url.trim() !== '') {
- try {
- new URL(tool.url);
- } catch {
- errors.push('URL must be a valid URL format');
- }
- }
-
- // Project URL validation (if provided)
- if (tool.projectUrl && tool.projectUrl.trim() !== '') {
- try {
- new URL(tool.projectUrl);
- } catch {
- errors.push('Project URL must be a valid URL format');
- }
- }
-
- // Array fields validation
- const arrayFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software'];
- arrayFields.forEach(field => {
- if (tool[field] !== undefined && tool[field] !== null && !Array.isArray(tool[field])) {
- errors.push(`${field} must be an array if provided`);
- }
- });
-
- // String fields validation (optional but must be strings if provided)
- const stringFields = ['icon', 'license', 'accessType'];
- stringFields.forEach(field => {
- if (tool[field] !== undefined && tool[field] !== null && typeof tool[field] !== 'string') {
- errors.push(`${field} must be a string if provided`);
- }
- });
-
- // Boolean field validation
- if (tool.knowledgebase !== undefined && tool.knowledgebase !== null && typeof tool.knowledgebase !== 'boolean') {
- errors.push('knowledgebase must be a boolean if provided');
- }
-
- return errors;
- }
-
- // Full validation for form submission and bulk validation (includes duplicate checks)
- function validateTool(tool, index = null) {
- const prefix = index !== null ? `Tool ${index + 1} (${tool.name || 'Unnamed'}): ` : '';
- const errors = validateToolBasic(tool).map(error => prefix + error);
-
- // Check for duplicate names (only for single tool validation)
- if (index === null) {
- const existingIndex = toolsData.tools.findIndex(t => t.name === tool.name);
- if (existingIndex >= 0 && existingIndex !== editingIndex) {
- errors.push(prefix + 'Tool name already exists');
- }
- }
-
- return errors;
- }
-
- // Update the renderTools function to use basic validation
- // Find this line in the renderTools function:
- // And replace it with:
- // const hasValidationIssues = validateToolBasic(tool).length > 0;
-
- function fixMissingUrls() {
- const toolsWithMissingUrls = toolsData.tools.filter(tool =>
- !tool.url || typeof tool.url !== 'string' || tool.url.trim() === ''
- );
-
- if (toolsWithMissingUrls.length === 0) {
- updateStatus('ā
No tools with missing URLs found');
+ if (!toolsData['domain-agnostic-software'] || toolsData['domain-agnostic-software'].length === 0) {
+ container.innerHTML = '
No categories found
';
return;
}
- const confirmMessage = `Found ${toolsWithMissingUrls.length} tools with missing URLs:\n\n${toolsWithMissingUrls.map(t => `⢠${t.name || 'Unnamed'}`).join('\n')}\n\nFix by adding placeholder URLs?`;
+ container.innerHTML = toolsData['domain-agnostic-software'].map((category, index) => `
+
+
${category.name}
+
${category.description || 'No description'}
+
+ ID: ${category.id} ${category.use_cases ? `⢠${category.use_cases.length} use cases` : ''}
+
+
+ `).join('');
+ }
+
+ function selectCategory(index) {
+ selectedCategory = toolsData['domain-agnostic-software'][index];
+ editingCategoryIndex = index;
- if (confirm(confirmMessage)) {
- let fixedCount = 0;
- toolsData.tools.forEach(tool => {
- if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
- tool.url = 'https://example.com'; // Placeholder URL
- fixedCount++;
- }
- });
-
- updateStatus(`š§ Fixed ${fixedCount} tools with missing URLs (added placeholder URLs)`);
- renderTools();
-
- // Show validation results
- setTimeout(() => validateAllTools(), 500);
+ // Populate form
+ document.getElementById('categoryId').value = selectedCategory.id || '';
+ document.getElementById('categoryName').value = selectedCategory.name || '';
+ document.getElementById('categoryDescription').value = selectedCategory.description || '';
+ setTagValues('useCases', selectedCategory.use_cases);
+
+ document.getElementById('deleteCategoryBtn').style.display = 'inline-block';
+ renderCategories();
+ }
+
+ function saveCategory() {
+ const category = {
+ id: document.getElementById('categoryId').value.trim(),
+ name: document.getElementById('categoryName').value.trim(),
+ description: document.getElementById('categoryDescription').value.trim(),
+ use_cases: getTagValues('useCases')
+ };
+
+ if (!category.id || !category.name) {
+ alert('Category ID and Name are required');
+ return;
}
+
+ // Remove empty description
+ if (!category.description) {
+ delete category.description;
+ }
+
+ // Remove empty use_cases array
+ if (category.use_cases.length === 0) {
+ delete category.use_cases;
+ }
+
+ if (editingCategoryIndex >= 0) {
+ toolsData['domain-agnostic-software'][editingCategoryIndex] = category;
+ updateStatus(`ā
Updated category ${category.name}`);
+ } else {
+ toolsData['domain-agnostic-software'].push(category);
+ updateStatus(`ā
Added category ${category.name}`);
+ }
+
+ renderCategories();
+ clearCategoryForm();
+ }
+
+ function deleteCategory() {
+ if (editingCategoryIndex >= 0 && confirm('Are you sure you want to delete this category?')) {
+ const categoryName = toolsData['domain-agnostic-software'][editingCategoryIndex].name;
+ toolsData['domain-agnostic-software'].splice(editingCategoryIndex, 1);
+ updateStatus(`Deleted category ${categoryName}`);
+ renderCategories();
+ clearCategoryForm();
+ }
+ }
+
+ function clearCategoryForm() {
+ document.getElementById('categoryForm').reset();
+ setTagValues('useCases', []);
+ selectedCategory = null;
+ editingCategoryIndex = -1;
+ document.getElementById('deleteCategoryBtn').style.display = 'none';
+ renderCategories();
}
function validateAllTools() {
@@ -1315,8 +1511,6 @@
} else {
updateStatus(`ā Found ${allErrors.length} validation errors`);
showValidationErrors(allErrors);
-
- // Scroll to errors
document.getElementById('validationErrors').scrollIntoView({ behavior: 'smooth' });
}
@@ -1327,7 +1521,6 @@
const errorDiv = document.getElementById('validationErrors');
if (Array.isArray(errors) && errors.length > 10) {
- // Show first 10 errors and a summary for large error lists
const firstTen = errors.slice(0, 10);
const remaining = errors.length - 10;
errorDiv.innerHTML = `
@@ -1351,49 +1544,45 @@
// Filtering and sorting
function filterTools() {
- const searchTerm = document.getElementById('searchInput').value.toLowerCase();
+ const rawSearch = document.getElementById('searchInput').value || '';
+ const searchTerm = rawSearch.toLowerCase();
const typeFilter = document.getElementById('typeFilter').value;
const skillFilter = document.getElementById('skillFilter').value;
const domainFilter = document.getElementById('domainFilter').value;
const knowledgebaseFilter = document.getElementById('knowledgebaseFilter').value;
const validationFilter = document.getElementById('validationFilter').value;
-
+
filteredTools = toolsData.tools.filter(tool => {
- const matchesSearch = !searchTerm ||
- tool.name.toLowerCase().includes(searchTerm) ||
- tool.description.toLowerCase().includes(searchTerm) ||
- (tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(searchTerm)));
-
+ // Safe access helpers
+ const name = (tool.name || '').toLowerCase();
+ const desc = (tool.description || '').toLowerCase();
+ const tagHit = Array.isArray(tool.tags) && tool.tags.some(tag => (tag || '').toLowerCase().includes(searchTerm));
+
+ const matchesSearch = !searchTerm || name.includes(searchTerm) || desc.includes(searchTerm) || tagHit;
const matchesType = !typeFilter || tool.type === typeFilter;
const matchesSkill = !skillFilter || tool.skillLevel === skillFilter;
- const matchesDomain = !domainFilter || (tool.domains && tool.domains.includes(domainFilter));
- const matchesKnowledgebase = !knowledgebaseFilter ||
- (knowledgebaseFilter === 'true' && tool.knowledgebase) ||
+ const matchesDomain = !domainFilter || (Array.isArray(tool.domains) && tool.domains.includes(domainFilter));
+ const matchesKnowledgebase = !knowledgebaseFilter ||
+ (knowledgebaseFilter === 'true' && !!tool.knowledgebase) ||
(knowledgebaseFilter === 'false' && !tool.knowledgebase);
-
- // Validation filter
+
let matchesValidation = true;
if (validationFilter) {
- const hasValidationIssues = validateToolBasic(tool).length > 0;
- if (validationFilter === 'valid') {
- matchesValidation = !hasValidationIssues;
- } else if (validationFilter === 'invalid') {
- matchesValidation = hasValidationIssues;
- }
+ const hasValidationIssues = validateTool(tool, toolsData.tools.indexOf(tool)).length > 0;
+ matchesValidation = validationFilter === 'invalid' ? hasValidationIssues : !hasValidationIssues;
}
-
+
return matchesSearch && matchesType && matchesSkill && matchesDomain && matchesKnowledgebase && matchesValidation;
});
-
+
sortTools();
}
+
function showInvalidToolsFilter(errorCount) {
- // Set validation filter to show only invalid tools
document.getElementById('validationFilter').value = 'invalid';
filterTools();
- // Show helpful message
showValidationErrors([
`ā ļø Import completed with ${errorCount} validation errors.`,
'',
@@ -1420,17 +1609,20 @@
function sortTools() {
const sortBy = document.getElementById('sortBy').value;
-
+
filteredTools.sort((a, b) => {
- let aVal = a[sortBy] || '';
- let bVal = b[sortBy] || '';
-
+ let aVal = a && a[sortBy];
+ let bVal = b && b[sortBy];
+
if (Array.isArray(aVal)) aVal = aVal.join(', ');
if (Array.isArray(bVal)) bVal = bVal.join(', ');
-
- return aVal.toString().localeCompare(bVal.toString());
+
+ aVal = (aVal == null ? '' : String(aVal));
+ bVal = (bVal == null ? '' : String(bVal));
+
+ return aVal.localeCompare(bVal);
});
-
+
renderTools();
}
@@ -1438,7 +1630,8 @@
function renderTools() {
const container = document.getElementById('toolsList');
const countElement = document.getElementById('toolCount');
- if (!filteredTools.length) {
+
+ if (!Array.isArray(filteredTools) || filteredTools.length === 0) {
countElement.textContent = 0;
container.innerHTML = '
No tools found
';
return;
@@ -1446,14 +1639,8 @@
countElement.textContent = filteredTools.length;
- if (filteredTools.length === 0) {
- container.innerHTML = '
No tools found
';
- return;
- }
-
container.innerHTML = filteredTools.map((tool, index) => {
- // Quick validation check for visual indicator
- const hasValidationIssues = validateToolBasic(tool).length > 0;
+ const hasValidationIssues = validateTool(tool, toolsData.tools.indexOf(tool)).length > 0;
return `
@@ -1481,6 +1668,11 @@
Domains: ${tool.domains.slice(0, 3).join(', ')}${tool.domains.length > 3 ? '...' : ''}
` : ''}
+ ${tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length ? `
+
+ Categories: ${tool['domain-agnostic-software'].slice(0, 2).join(', ')}${tool['domain-agnostic-software'].length > 2 ? '...' : ''}
+
+ ` : ''}
${tool.tags ? `
${tool.tags.slice(0, 5).map(tag => `${tag}`).join('')}
@@ -1523,7 +1715,8 @@
'Methods': toolsData.tools.filter(t => t.type === 'method').length,
'Concepts': toolsData.tools.filter(t => t.type === 'concept').length,
'With KB': toolsData.tools.filter(t => t.knowledgebase).length,
- 'Open Source': toolsData.tools.filter(t => t.license && t.license !== 'Proprietary').length
+ 'Open Source': toolsData.tools.filter(t => t.license && t.license !== 'Proprietary').length,
+ 'Categories': toolsData['domain-agnostic-software'].length
};
return stats;
@@ -1639,7 +1832,6 @@
}
function getSelectedTools() {
- // For now, return all tools. Could be enhanced to support multi-selection
return toolsData.tools;
}
@@ -1659,7 +1851,6 @@
return;
}
- // Show import summary and confirm
const importSummary = `
Import Summary:
⢠${parsed.tools.length} tools
@@ -1674,7 +1865,6 @@ This will replace all current data. Continue?`;
return;
}
- // Import the data
toolsData = parsed;
// Ensure required structure exists
@@ -1686,7 +1876,6 @@ This will replace all current data. Continue?`;
populateFilters();
- // Check for validation errors after import
updateStatus('Validating imported data...');
const validationErrors = [];
@@ -1708,15 +1897,17 @@ This will replace all current data. Continue?`;
});
if (validationErrors.length > 0) {
- // Show tools with validation issues
- showInvalidToolsFilter(validationErrors.length);
+ showInvalidToolsFilter(validationErrors.length); // this already calls filterTools()
} else {
- // Everything is valid
hideValidationErrors();
- renderTools();
+ // ā
show everything by default
+ filteredTools = toolsData.tools.slice();
+ sortTools(); // will render afterwards
updateStatus(`ā
Successfully imported ${toolsData.tools.length} tools - all valid`);
}
+ renderCategories();
+
} catch (error) {
alert('Error parsing YAML: ' + error.message);
updateStatus('ā YAML parsing failed');
@@ -1726,7 +1917,6 @@ This will replace all current data. Continue?`;
}
function exportYAML() {
- // Check for validation errors before export
const allErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
@@ -1737,24 +1927,17 @@ This will replace all current data. Continue?`;
const confirmExport = confirm(`ā ļø Warning: Found ${allErrors.length} validation errors in the database.\n\nExporting this data may cause issues when importing into the web application.\n\nWould you like to:\n⢠Click OK to export anyway\n⢠Click Cancel to fix errors first (recommended)`);
if (!confirmExport) {
- // Set filter to show invalid tools and highlight the issues
showInvalidToolsFilter(allErrors.length);
return;
}
}
try {
- // Process tools to ensure proper description formatting
const processedData = {
...toolsData,
tools: toolsData.tools.map(tool => {
const processedTool = { ...tool };
- // Format description with folded style for multiline
- if (processedTool.description && processedTool.description.length > 60) {
- // Keep the description as is, jsyaml will handle the formatting
- }
-
// Clean up empty arrays and null values
Object.keys(processedTool).forEach(key => {
if (Array.isArray(processedTool[key]) && processedTool[key].length === 0) {
@@ -1775,38 +1958,26 @@ This will replace all current data. Continue?`;
noRefs: true,
sortKeys: false,
styles: {
- '!!str': 'fold' // Use folded style for long strings
- },
- replacer: (key, value) => {
- // Handle description field specially
- if (key === 'description' && typeof value === 'string' && value.length > 60) {
- return value;
- }
- return value;
+ '!!str': 'fold'
}
});
- // Post-process to ensure descriptions use >- format
const lines = yamlContent.split('\n');
const processedLines = [];
- let inDescription = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith('description:')) {
if (line.includes('"') || line.includes("'")) {
- // Replace quoted description with folded style
const indent = line.match(/^(\s*)/)[1];
processedLines.push(`${indent}description: >-`);
- // Extract the description content
let descContent = line.substring(line.indexOf(':') + 1).trim();
if (descContent.startsWith('"') || descContent.startsWith("'")) {
descContent = descContent.slice(1, -1);
}
- // Split and reformat the description
const words = descContent.split(' ');
let currentLine = '';
const descIndent = indent + ' ';
@@ -1857,37 +2028,41 @@ This will replace all current data. Continue?`;
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
- // Ctrl/Cmd + S to save
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
- saveTool();
+ if (currentSection === 'tools') {
+ saveTool();
+ } else if (currentSection === 'categories') {
+ saveCategory();
+ }
}
- // Ctrl/Cmd + N to add new tool
if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
event.preventDefault();
- addNewTool();
+ if (currentSection === 'tools') {
+ addNewTool();
+ }
}
- // Ctrl/Cmd + E to export
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
event.preventDefault();
exportYAML();
}
- // Alt + V to validate all (changed from Ctrl+V to avoid paste conflict)
if (event.altKey && event.key === 'v') {
event.preventDefault();
validateAllTools();
}
- // Escape to clear selection/form
if (event.key === 'Escape') {
clearSelection();
- clearForm();
+ if (currentSection === 'tools') {
+ clearForm();
+ } else if (currentSection === 'categories') {
+ clearCategoryForm();
+ }
}
- // Ctrl/Cmd + F to focus search
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
event.preventDefault();
document.getElementById('searchInput').focus();