diff --git a/tools-yaml-editor.html b/tools-yaml-editor.html index 98e4252..60dc576 100644 --- a/tools-yaml-editor.html +++ b/tools-yaml-editor.html @@ -45,10 +45,11 @@ background-color: var(--bg-primary); color: var(--text-primary); line-height: 1.5; + min-width: 1200px; } .container { - max-width: 1400px; + max-width: 1600px; margin: 0 auto; padding: 20px; } @@ -95,7 +96,7 @@ .main-content { display: grid; - grid-template-columns: 1fr 400px; + grid-template-columns: 1fr 500px; gap: 20px; } @@ -407,19 +408,71 @@ flex: 1; } - @media (max-width: 1200px) { - .main-content { - grid-template-columns: 1fr; - } - - .edit-section { - position: static; - max-height: none; - } + .section-tabs { + display: flex; + margin-bottom: 20px; + border-bottom: 1px solid var(--border-color); + } - .search-filters { - grid-template-columns: 1fr; - } + .section-tab { + padding: 10px 20px; + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; + } + + .section-tab.active { + color: var(--accent-color); + border-bottom-color: var(--accent-color); + } + + .section-content { + display: none; + } + + .section-content.active { + display: block; + } + + .categories-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 4px; + margin-bottom: 15px; + } + + .category-item { + padding: 10px; + border-bottom: 1px solid var(--border-color); + cursor: pointer; + transition: background 0.2s; + } + + .category-item:hover { + background: var(--bg-tertiary); + } + + .category-item:last-child { + border-bottom: none; + } + + .category-item.selected { + background: var(--bg-tertiary); + border-left: 3px solid var(--accent-color); + } + + .category-name { + font-weight: 600; + margin-bottom: 4px; + } + + .category-description { + font-size: 0.9em; + color: var(--text-secondary); } @@ -499,7 +552,6 @@
- @@ -546,146 +598,221 @@
-

Tool Editor

-
-
- - -
+
+ + + + + +
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
-
- -
-
+ +
+

Tool Editor

+ +
+ +
-
-
- -
-
- -
-
+
+ +
-
-
- -
-
- -
-
+
+ +
-
-
- -
-
- -
-
+
+ +
-
-
- -
- - +
+ +
-
-
- -
- - +
+ +
-
-
- -
+
+ + +
-
- - - -
- +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ + + +
+ +
+ + +
+

Domain-Agnostic Software Categories

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+
+
+ + + +
+
+
+ + +
+

Domains Management

+

Domain management interface would go here...

+
+ +
+

Phases Management

+

Phases management interface would go here...

+
+ +
+

Scenarios Management

+

Scenarios management interface would go here...

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