fix contrib mechanic
This commit is contained in:
parent
2fcc84991a
commit
4cc3e2c830
@ -5,6 +5,7 @@ import '../styles/global.css';
|
||||
import '../styles/auditTrail.css';
|
||||
import '../styles/knowledgebase.css';
|
||||
import '../styles/palette.css';
|
||||
import '../styles/autocomplete.css';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
|
||||
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses + related_software)
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
@ -27,6 +27,7 @@ const ContributionToolSchema = z.object({
|
||||
knowledgebase: z.boolean().optional().nullable(),
|
||||
'domain-agnostic-software': z.array(z.string()).optional().nullable(),
|
||||
related_concepts: z.array(z.string()).optional().nullable(),
|
||||
related_software: z.array(z.string()).optional().nullable(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
statusUrl: z.string().url('Must be a valid URL').optional().nullable()
|
||||
});
|
||||
@ -80,6 +81,38 @@ function sanitizeInput(obj: any): any {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function preprocessFormData(body: any): any {
|
||||
// Handle comma-separated strings from autocomplete inputs
|
||||
if (body.tool) {
|
||||
// Handle tags
|
||||
if (typeof body.tool.tags === 'string') {
|
||||
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related concepts
|
||||
if (body.tool.relatedConcepts) {
|
||||
if (typeof body.tool.relatedConcepts === 'string') {
|
||||
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
} else {
|
||||
body.tool.related_concepts = body.tool.relatedConcepts;
|
||||
}
|
||||
delete body.tool.relatedConcepts; // Remove the original key
|
||||
}
|
||||
|
||||
// Handle related software
|
||||
if (body.tool.relatedSoftware) {
|
||||
if (typeof body.tool.relatedSoftware === 'string') {
|
||||
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
|
||||
} else {
|
||||
body.tool.related_software = body.tool.relatedSoftware;
|
||||
}
|
||||
delete body.tool.relatedSoftware; // Remove the original key
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
|
||||
const errors: string[] = [];
|
||||
|
||||
@ -109,6 +142,17 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
|
||||
}
|
||||
}
|
||||
|
||||
// Validate related items exist (optional validation - could be enhanced)
|
||||
if (tool.related_concepts && tool.related_concepts.length > 0) {
|
||||
// Could validate that referenced concepts actually exist
|
||||
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
|
||||
}
|
||||
|
||||
if (tool.related_software && tool.related_software.length > 0) {
|
||||
// Could validate that referenced software actually exists
|
||||
console.log('[VALIDATION] Related software provided:', tool.related_software);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
|
||||
} catch (error) {
|
||||
@ -143,6 +187,9 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiSpecial.invalidJSON();
|
||||
}
|
||||
|
||||
// Preprocess form data to handle autocomplete inputs
|
||||
body = preprocessFormData(body);
|
||||
|
||||
const sanitizedBody = sanitizeInput(body);
|
||||
|
||||
let validatedData;
|
||||
@ -153,6 +200,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const errorMessages = error.errors.map(err =>
|
||||
`${err.path.join('.')}: ${err.message}`
|
||||
);
|
||||
console.log('[VALIDATION] Zod validation errors:', errorMessages);
|
||||
return apiError.validation('Validation failed', errorMessages);
|
||||
}
|
||||
|
||||
@ -174,6 +222,16 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[CONTRIBUTION] Processing contribution:', {
|
||||
type: contributionData.type,
|
||||
toolName: contributionData.tool.name,
|
||||
toolType: contributionData.tool.type,
|
||||
submitter: userEmail,
|
||||
hasRelatedConcepts: !!(contributionData.tool.related_concepts?.length),
|
||||
hasRelatedSoftware: !!(contributionData.tool.related_software?.length),
|
||||
tagsCount: contributionData.tool.tags?.length || 0
|
||||
});
|
||||
|
||||
try {
|
||||
const gitManager = new GitContributionManager();
|
||||
const result = await gitManager.submitContribution(contributionData);
|
||||
|
@ -22,6 +22,17 @@ const existingTools = data.tools;
|
||||
const editToolName = Astro.url.searchParams.get('edit');
|
||||
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
|
||||
const isEdit = !!editTool;
|
||||
|
||||
// Extract data for autocomplete
|
||||
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
|
||||
const allSoftwareAndMethods = existingTools
|
||||
.filter(tool => tool.type === 'software' || tool.type === 'method')
|
||||
.map(tool => tool.name)
|
||||
.sort();
|
||||
const allConcepts = existingTools
|
||||
.filter(tool => tool.type === 'concept')
|
||||
.map(tool => tool.name)
|
||||
.sort();
|
||||
---
|
||||
|
||||
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
|
||||
@ -194,16 +205,27 @@ const isEdit = !!editTool;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="concepts-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
|
||||
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Konzepte im Zusammenhang</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
|
||||
{existingTools.filter(tool => tool.type === 'concept').map(concept => (
|
||||
<label class="checkbox-wrapper">
|
||||
<input type="checkbox" name="relatedConcepts" value={concept.name}
|
||||
checked={editTool?.related_concepts?.includes(concept.name)} />
|
||||
<span>{concept.name}</span>
|
||||
</label>
|
||||
))}
|
||||
<div id="relations-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
|
||||
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Verwandte Tools & Konzepte</h3>
|
||||
|
||||
<div style="display: grid; gap: 1.5rem;">
|
||||
<div id="related-concepts-section">
|
||||
<label for="related-concepts-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Konzepte</label>
|
||||
<input type="text" id="related-concepts-input" placeholder="Beginne zu tippen, um Konzepte zu finden..." />
|
||||
<input type="hidden" id="related-concepts-hidden" name="relatedConcepts" value={editTool?.related_concepts?.join(', ') || ''} />
|
||||
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
|
||||
Konzepte, die mit diesem Tool/Methode in Verbindung stehen
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div id="related-software-section">
|
||||
<label for="related-software-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Software & Methoden</label>
|
||||
<input type="text" id="related-software-input" placeholder="Beginne zu tippen, um Software/Methoden zu finden..." />
|
||||
<input type="hidden" id="related-software-hidden" name="relatedSoftware" value={editTool?.related_software?.join(', ') || ''} />
|
||||
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
|
||||
Software oder Methoden, die oft zusammen mit diesem Tool verwendet werden
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -211,9 +233,12 @@ const isEdit = !!editTool;
|
||||
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Zusatzinfos</h3>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<label for="tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
|
||||
<input type="text" id="tags" name="tags" value={editTool?.tags?.join(', ') || ''}
|
||||
placeholder="Komma-getrennt: Passende Begriffe, nach denen ihr suchen würdet." />
|
||||
<label for="tags-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
|
||||
<input type="text" id="tags-input" placeholder="Beginne zu tippen, um Tags hinzuzufügen..." />
|
||||
<input type="hidden" id="tags-hidden" name="tags" value={editTool?.tags?.join(', ') || ''} />
|
||||
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
|
||||
Passende Begriffe, nach denen ihr suchen würdet
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
@ -274,7 +299,275 @@ const isEdit = !!editTool;
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
|
||||
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
|
||||
// Consolidated Autocomplete Functionality - inlined to avoid module loading issues
|
||||
class AutocompleteManager {
|
||||
constructor(inputElement, dataSource, options = {}) {
|
||||
this.input = inputElement;
|
||||
this.dataSource = dataSource;
|
||||
this.options = {
|
||||
minLength: 1,
|
||||
maxResults: 10,
|
||||
placeholder: 'Type to search...',
|
||||
allowMultiple: false,
|
||||
separator: ', ',
|
||||
filterFunction: this.defaultFilter.bind(this),
|
||||
renderFunction: this.defaultRender.bind(this),
|
||||
...options
|
||||
};
|
||||
|
||||
this.isOpen = false;
|
||||
this.selectedIndex = -1;
|
||||
this.filteredData = [];
|
||||
this.selectedItems = new Set();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
|
||||
if (this.options.allowMultiple) {
|
||||
this.initMultipleMode();
|
||||
}
|
||||
}
|
||||
|
||||
createDropdown() {
|
||||
this.dropdown = document.createElement('div');
|
||||
this.dropdown.className = 'autocomplete-dropdown';
|
||||
|
||||
// Insert dropdown after input
|
||||
this.input.parentNode.style.position = 'relative';
|
||||
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.input.addEventListener('input', (e) => {
|
||||
this.handleInput(e.target.value);
|
||||
});
|
||||
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
this.handleKeydown(e);
|
||||
});
|
||||
|
||||
this.input.addEventListener('focus', () => {
|
||||
if (this.input.value.length >= this.options.minLength) {
|
||||
this.showDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', (e) => {
|
||||
// Delay to allow click events on dropdown items
|
||||
setTimeout(() => {
|
||||
if (!this.dropdown.contains(document.activeElement)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initMultipleMode() {
|
||||
this.selectedContainer = document.createElement('div');
|
||||
this.selectedContainer.className = 'autocomplete-selected';
|
||||
|
||||
this.input.parentNode.insertBefore(this.selectedContainer, this.input);
|
||||
this.updateSelectedDisplay();
|
||||
}
|
||||
|
||||
handleInput(value) {
|
||||
if (value.length >= this.options.minLength) {
|
||||
this.filteredData = this.options.filterFunction(value);
|
||||
this.selectedIndex = -1;
|
||||
this.renderDropdown();
|
||||
this.showDropdown();
|
||||
} else {
|
||||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(e) {
|
||||
if (!this.isOpen) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
|
||||
this.updateHighlight();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
||||
this.updateHighlight();
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.selectItem(this.filteredData[this.selectedIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
this.hideDropdown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
defaultFilter(query) {
|
||||
const searchTerm = query.toLowerCase();
|
||||
return this.dataSource
|
||||
.filter(item => {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
return text.toLowerCase().includes(searchTerm) &&
|
||||
(!this.options.allowMultiple || !this.selectedItems.has(text));
|
||||
})
|
||||
.slice(0, this.options.maxResults);
|
||||
}
|
||||
|
||||
defaultRender(item) {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
|
||||
}
|
||||
|
||||
renderDropdown() {
|
||||
if (this.filteredData.length === 0) {
|
||||
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdown.innerHTML = this.filteredData
|
||||
.map((item, index) => {
|
||||
const content = this.options.renderFunction(item);
|
||||
return `<div class="autocomplete-option" data-index="${index}">${content}</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Bind click events
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
option.addEventListener('click', () => {
|
||||
this.selectItem(this.filteredData[index]);
|
||||
});
|
||||
|
||||
option.addEventListener('mouseenter', () => {
|
||||
this.selectedIndex = index;
|
||||
this.updateHighlight();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateHighlight() {
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
option.style.backgroundColor = index === this.selectedIndex
|
||||
? 'var(--color-bg-secondary)'
|
||||
: 'transparent';
|
||||
});
|
||||
}
|
||||
|
||||
selectItem(item) {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
|
||||
if (this.options.allowMultiple) {
|
||||
this.selectedItems.add(text);
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
this.input.value = '';
|
||||
} else {
|
||||
this.input.value = text;
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||
}));
|
||||
}
|
||||
|
||||
removeItem(text) {
|
||||
if (this.options.allowMultiple) {
|
||||
this.selectedItems.delete(text);
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedDisplay() {
|
||||
if (!this.options.allowMultiple || !this.selectedContainer) return;
|
||||
|
||||
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
|
||||
.map(item => `
|
||||
<span class="autocomplete-tag">
|
||||
${this.escapeHtml(item)}
|
||||
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}">×</button>
|
||||
</span>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Bind remove events
|
||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.removeItem(btn.getAttribute('data-item'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateInputValue() {
|
||||
if (this.options.allowMultiple && this.options.hiddenInput) {
|
||||
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator);
|
||||
}
|
||||
}
|
||||
|
||||
showDropdown() {
|
||||
this.dropdown.style.display = 'block';
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
hideDropdown() {
|
||||
this.dropdown.style.display = 'none';
|
||||
this.isOpen = false;
|
||||
this.selectedIndex = -1;
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
setDataSource(newDataSource) {
|
||||
this.dataSource = newDataSource;
|
||||
}
|
||||
|
||||
getSelectedItems() {
|
||||
return Array.from(this.selectedItems);
|
||||
}
|
||||
|
||||
setSelectedItems(items) {
|
||||
this.selectedItems = new Set(items);
|
||||
if (this.options.allowMultiple) {
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.dropdown && this.dropdown.parentNode) {
|
||||
this.dropdown.parentNode.removeChild(this.dropdown);
|
||||
}
|
||||
if (this.selectedContainer && this.selectedContainer.parentNode) {
|
||||
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[FORM] Script loaded, initializing...');
|
||||
|
||||
class ContributionForm {
|
||||
@ -283,6 +576,7 @@ class ContributionForm {
|
||||
this.editTool = editTool;
|
||||
this.elements = {};
|
||||
this.isSubmitting = false;
|
||||
this.autocompleteManagers = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -303,14 +597,20 @@ class ContributionForm {
|
||||
yamlPreview: document.getElementById('yaml-preview'),
|
||||
successModal: document.getElementById('success-modal'),
|
||||
softwareFields: document.getElementById('software-fields'),
|
||||
conceptsFields: document.getElementById('concepts-fields'),
|
||||
relationsFields: document.getElementById('relations-fields'),
|
||||
descriptionCount: document.getElementById('description-count'),
|
||||
reasonCount: document.getElementById('reason-count'),
|
||||
validationErrors: document.getElementById('validation-errors'),
|
||||
errorList: document.getElementById('error-list'),
|
||||
platformsRequired: document.getElementById('platforms-required'),
|
||||
licenseRequired: document.getElementById('license-required'),
|
||||
licenseInput: document.getElementById('license')
|
||||
licenseInput: document.getElementById('license'),
|
||||
tagsInput: document.getElementById('tags-input'),
|
||||
tagsHidden: document.getElementById('tags-hidden'),
|
||||
relatedConceptsInput: document.getElementById('related-concepts-input'),
|
||||
relatedConceptsHidden: document.getElementById('related-concepts-hidden'),
|
||||
relatedSoftwareInput: document.getElementById('related-software-input'),
|
||||
relatedSoftwareHidden: document.getElementById('related-software-hidden')
|
||||
};
|
||||
|
||||
if (!this.elements.form || !this.elements.submitBtn) {
|
||||
@ -327,6 +627,7 @@ class ContributionForm {
|
||||
|
||||
console.log('[FORM] Setting up handlers...');
|
||||
this.setupEventListeners();
|
||||
this.setupAutocomplete();
|
||||
this.updateFieldVisibility();
|
||||
this.setupCharacterCounters();
|
||||
this.updateYAMLPreview();
|
||||
@ -334,6 +635,65 @@ class ContributionForm {
|
||||
console.log('[FORM] Initialization complete!');
|
||||
}
|
||||
|
||||
setupAutocomplete() {
|
||||
// Tags autocomplete
|
||||
if (this.elements.tagsInput && this.elements.tagsHidden) {
|
||||
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
|
||||
allowMultiple: true,
|
||||
hiddenInput: this.elements.tagsHidden,
|
||||
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.tags) {
|
||||
tagsManager.setSelectedItems(this.editTool.tags);
|
||||
}
|
||||
|
||||
this.autocompleteManagers.set('tags', tagsManager);
|
||||
}
|
||||
|
||||
// Related concepts autocomplete
|
||||
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
|
||||
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
|
||||
allowMultiple: true,
|
||||
hiddenInput: this.elements.relatedConceptsHidden,
|
||||
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.related_concepts) {
|
||||
conceptsManager.setSelectedItems(this.editTool.related_concepts);
|
||||
}
|
||||
|
||||
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
|
||||
}
|
||||
|
||||
// Related software autocomplete
|
||||
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
|
||||
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
|
||||
allowMultiple: true,
|
||||
hiddenInput: this.elements.relatedSoftwareHidden,
|
||||
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
|
||||
});
|
||||
|
||||
// Set initial values if editing
|
||||
if (this.editTool?.related_software) {
|
||||
softwareManager.setSelectedItems(this.editTool.related_software);
|
||||
}
|
||||
|
||||
this.autocompleteManagers.set('relatedSoftware', softwareManager);
|
||||
}
|
||||
|
||||
// Listen for autocomplete changes to update YAML preview
|
||||
Object.values(this.autocompleteManagers).forEach(manager => {
|
||||
if (manager.input) {
|
||||
manager.input.addEventListener('autocomplete:select', () => {
|
||||
this.updateYAMLPreview();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.elements.typeSelect.addEventListener('change', () => {
|
||||
this.updateFieldVisibility();
|
||||
@ -367,18 +727,34 @@ updateFieldVisibility() {
|
||||
const type = this.elements.typeSelect.value;
|
||||
|
||||
this.elements.softwareFields.style.display = 'none';
|
||||
this.elements.conceptsFields.style.display = 'none';
|
||||
this.elements.relationsFields.style.display = 'none';
|
||||
|
||||
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
|
||||
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
|
||||
|
||||
if (type === 'software') {
|
||||
this.elements.softwareFields.style.display = 'block';
|
||||
this.elements.conceptsFields.style.display = 'block';
|
||||
this.elements.relationsFields.style.display = 'block';
|
||||
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
|
||||
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
|
||||
} else if (type === 'method') {
|
||||
this.elements.conceptsFields.style.display = 'block';
|
||||
this.elements.relationsFields.style.display = 'block';
|
||||
} else if (type === 'concept') {
|
||||
// Concepts can only relate to software/methods, not other concepts
|
||||
this.elements.relationsFields.style.display = 'block';
|
||||
// Hide concepts section for concept types
|
||||
const conceptsSection = document.getElementById('related-concepts-section');
|
||||
const softwareSection = document.getElementById('related-software-section');
|
||||
if (conceptsSection) conceptsSection.style.display = 'none';
|
||||
if (softwareSection) softwareSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Show appropriate relation sections
|
||||
if (type !== 'concept') {
|
||||
const conceptsSection = document.getElementById('related-concepts-section');
|
||||
const softwareSection = document.getElementById('related-software-section');
|
||||
if (conceptsSection) conceptsSection.style.display = 'block';
|
||||
if (softwareSection) softwareSection.style.display = 'block';
|
||||
}
|
||||
|
||||
console.log('[FORM] Field visibility updated for type:', type);
|
||||
@ -440,14 +816,22 @@ updateYAMLPreview() {
|
||||
tool.knowledgebase = true;
|
||||
}
|
||||
|
||||
const tags = formData.get('tags');
|
||||
if (tags) {
|
||||
tool.tags = tags.split(',').map(t => t.trim()).filter(Boolean);
|
||||
// Handle tags from autocomplete
|
||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||
if (tagsValue) {
|
||||
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
const relatedConcepts = formData.getAll('relatedConcepts');
|
||||
if (relatedConcepts.length > 0) {
|
||||
tool.related_concepts = relatedConcepts;
|
||||
// Handle related concepts from autocomplete
|
||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||
if (relatedConceptsValue) {
|
||||
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related software from autocomplete
|
||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||
if (relatedSoftwareValue) {
|
||||
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
const yaml = this.generateYAML(tool);
|
||||
@ -486,6 +870,9 @@ generateYAML(tool) {
|
||||
if (tool.related_concepts && tool.related_concepts.length > 0) {
|
||||
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
|
||||
}
|
||||
if (tool.related_software && tool.related_software.length > 0) {
|
||||
lines.push(`related_software: [${tool.related_software.map(s => `"${s}"`).join(', ')}]`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@ -560,6 +947,7 @@ showValidationErrors(errors) {
|
||||
|
||||
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
async handleSubmit() {
|
||||
console.log('[FORM] Submit handler called!');
|
||||
|
||||
@ -597,14 +985,32 @@ showValidationErrors(errors) {
|
||||
phases: formData.getAll('phases'),
|
||||
skillLevel: formData.get('skillLevel'),
|
||||
url: formData.get('url'),
|
||||
tags: formData.get('tags') ?
|
||||
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
|
||||
tags: []
|
||||
},
|
||||
metadata: {
|
||||
reason: formData.get('reason') || ''
|
||||
reason: formData.get('reason') || '',
|
||||
contact: formData.get('contact') || ''
|
||||
}
|
||||
};
|
||||
|
||||
// Handle tags from autocomplete
|
||||
const tagsValue = this.elements.tagsHidden?.value || '';
|
||||
if (tagsValue) {
|
||||
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related concepts from autocomplete
|
||||
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
|
||||
if (relatedConceptsValue) {
|
||||
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Handle related software from autocomplete
|
||||
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
|
||||
if (relatedSoftwareValue) {
|
||||
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (formData.get('icon')) submission.tool.icon = formData.get('icon');
|
||||
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true;
|
||||
|
||||
@ -620,13 +1026,6 @@ showValidationErrors(errors) {
|
||||
}
|
||||
}
|
||||
|
||||
if (submission.tool.type !== 'concept') {
|
||||
const related = formData.getAll('relatedConcepts');
|
||||
if (related.length > 0) {
|
||||
submission.tool.related_concepts = related;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[FORM] Sending submission:', submission);
|
||||
|
||||
const response = await fetch('/api/contribute/tool', {
|
||||
@ -681,6 +1080,14 @@ showValidationErrors(errors) {
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clean up autocomplete managers
|
||||
this.autocompleteManagers.forEach(manager => {
|
||||
manager.destroy();
|
||||
});
|
||||
this.autocompleteManagers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeForm() {
|
||||
@ -707,3 +1114,4 @@ if (document.readyState === 'loading') {
|
||||
|
||||
console.log('[FORM] Script loaded successfully');
|
||||
</script>
|
||||
</BaseLayout>
|
121
src/styles/autocomplete.css
Normal file
121
src/styles/autocomplete.css
Normal file
@ -0,0 +1,121 @@
|
||||
/* ============================================================================
|
||||
AUTOCOMPLETE COMPONENT STYLES
|
||||
============================================================================ */
|
||||
|
||||
.autocomplete-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autocomplete-option {
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.autocomplete-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-option:hover {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.autocomplete-no-results {
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.autocomplete-selected {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.autocomplete-tag {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
}
|
||||
|
||||
.autocomplete-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.autocomplete-remove:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Animation for tag appearance */
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure autocomplete container has relative positioning */
|
||||
.autocomplete-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for autocomplete dropdown */
|
||||
.autocomplete-dropdown::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.autocomplete-dropdown::-webkit-scrollbar-track {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.autocomplete-dropdown::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.autocomplete-dropdown::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-secondary);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// src/utils/clientUtils.js
|
||||
// src/utils/clientUtils.ts
|
||||
// Client-side utilities that mirror server-side toolHelpers.ts
|
||||
|
||||
export function createToolSlug(toolName) {
|
||||
export function createToolSlug(toolName: string): string {
|
||||
if (!toolName || typeof toolName !== 'string') {
|
||||
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
|
||||
return '';
|
||||
@ -14,18 +14,353 @@ export function createToolSlug(toolName) {
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
export function findToolByIdentifier(tools, identifier) {
|
||||
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
|
||||
if (!identifier || !Array.isArray(tools)) return undefined;
|
||||
|
||||
return tools.find(tool =>
|
||||
return tools.find((tool: any) =>
|
||||
tool.name === identifier ||
|
||||
createToolSlug(tool.name) === identifier.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function isToolHosted(tool) {
|
||||
export function isToolHosted(tool: any): boolean {
|
||||
return tool.projectUrl !== undefined &&
|
||||
tool.projectUrl !== null &&
|
||||
tool.projectUrl !== "" &&
|
||||
tool.projectUrl.trim() !== "";
|
||||
}
|
||||
|
||||
// Consolidated Autocomplete Functionality
|
||||
interface AutocompleteOptions {
|
||||
minLength?: number;
|
||||
maxResults?: number;
|
||||
placeholder?: string;
|
||||
allowMultiple?: boolean;
|
||||
separator?: string;
|
||||
filterFunction?: (query: string) => any[];
|
||||
renderFunction?: (item: any) => string;
|
||||
hiddenInput?: HTMLInputElement;
|
||||
}
|
||||
|
||||
export class AutocompleteManager {
|
||||
public input: HTMLInputElement;
|
||||
public dataSource: any[];
|
||||
public options: AutocompleteOptions;
|
||||
public isOpen: boolean = false;
|
||||
public selectedIndex: number = -1;
|
||||
public filteredData: any[] = [];
|
||||
public selectedItems: Set<string> = new Set();
|
||||
public dropdown!: HTMLElement;
|
||||
public selectedContainer!: HTMLElement;
|
||||
|
||||
constructor(inputElement: HTMLInputElement, dataSource: any[], options: AutocompleteOptions = {}) {
|
||||
this.input = inputElement;
|
||||
this.dataSource = dataSource;
|
||||
this.options = {
|
||||
minLength: 1,
|
||||
maxResults: 10,
|
||||
placeholder: 'Type to search...',
|
||||
allowMultiple: false,
|
||||
separator: ', ',
|
||||
filterFunction: this.defaultFilter.bind(this),
|
||||
renderFunction: this.defaultRender.bind(this),
|
||||
...options
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init(): void {
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
|
||||
if (this.options.allowMultiple) {
|
||||
this.initMultipleMode();
|
||||
}
|
||||
}
|
||||
|
||||
createDropdown(): void {
|
||||
this.dropdown = document.createElement('div');
|
||||
this.dropdown.className = 'autocomplete-dropdown';
|
||||
this.dropdown.style.cssText = `
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
// Insert dropdown after input
|
||||
const parentElement = this.input.parentNode as HTMLElement;
|
||||
parentElement.style.position = 'relative';
|
||||
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
|
||||
}
|
||||
|
||||
bindEvents(): void {
|
||||
this.input.addEventListener('input', (e) => {
|
||||
this.handleInput((e.target as HTMLInputElement).value);
|
||||
});
|
||||
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
this.handleKeydown(e);
|
||||
});
|
||||
|
||||
this.input.addEventListener('focus', () => {
|
||||
if (this.input.value.length >= (this.options.minLength || 1)) {
|
||||
this.showDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', () => {
|
||||
// Delay to allow click events on dropdown items
|
||||
setTimeout(() => {
|
||||
const activeElement = document.activeElement;
|
||||
if (!activeElement || !this.dropdown.contains(activeElement)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
}, 150);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target as Node;
|
||||
if (!this.input.contains(target) && !this.dropdown.contains(target)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initMultipleMode(): void {
|
||||
this.selectedContainer = document.createElement('div');
|
||||
this.selectedContainer.className = 'autocomplete-selected';
|
||||
this.selectedContainer.style.cssText = `
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
min-height: 1.5rem;
|
||||
`;
|
||||
|
||||
const parentElement = this.input.parentNode as HTMLElement;
|
||||
parentElement.insertBefore(this.selectedContainer, this.input);
|
||||
this.updateSelectedDisplay();
|
||||
}
|
||||
|
||||
handleInput(value: string): void {
|
||||
if (value.length >= (this.options.minLength || 1)) {
|
||||
this.filteredData = this.options.filterFunction!(value);
|
||||
this.selectedIndex = -1;
|
||||
this.renderDropdown();
|
||||
this.showDropdown();
|
||||
} else {
|
||||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeydown(e: KeyboardEvent): void {
|
||||
if (!this.isOpen) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
|
||||
this.updateHighlight();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
||||
this.updateHighlight();
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.selectedIndex >= 0) {
|
||||
this.selectItem(this.filteredData[this.selectedIndex]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
this.hideDropdown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
defaultFilter(query: string): any[] {
|
||||
const searchTerm = query.toLowerCase();
|
||||
return this.dataSource
|
||||
.filter(item => {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
return text.toLowerCase().includes(searchTerm) &&
|
||||
(!this.options.allowMultiple || !this.selectedItems.has(text));
|
||||
})
|
||||
.slice(0, this.options.maxResults || 10);
|
||||
}
|
||||
|
||||
defaultRender(item: any): string {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
|
||||
}
|
||||
|
||||
renderDropdown(): void {
|
||||
if (this.filteredData.length === 0) {
|
||||
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdown.innerHTML = this.filteredData
|
||||
.map((item, index) => {
|
||||
const content = this.options.renderFunction!(item);
|
||||
return `<div class="autocomplete-option" data-index="${index}" style="
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
transition: background-color 0.15s ease;
|
||||
">${content}</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Bind click events
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
option.addEventListener('click', () => {
|
||||
this.selectItem(this.filteredData[index]);
|
||||
});
|
||||
|
||||
option.addEventListener('mouseenter', () => {
|
||||
this.selectedIndex = index;
|
||||
this.updateHighlight();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateHighlight(): void {
|
||||
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
|
||||
(option as HTMLElement).style.backgroundColor = index === this.selectedIndex
|
||||
? 'var(--color-bg-secondary)'
|
||||
: 'transparent';
|
||||
});
|
||||
}
|
||||
|
||||
selectItem(item: any): void {
|
||||
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
|
||||
|
||||
if (this.options.allowMultiple) {
|
||||
this.selectedItems.add(text);
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
this.input.value = '';
|
||||
} else {
|
||||
this.input.value = text;
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
// Trigger change event
|
||||
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
|
||||
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
|
||||
}));
|
||||
}
|
||||
|
||||
removeItem(text: string): void {
|
||||
if (this.options.allowMultiple) {
|
||||
this.selectedItems.delete(text);
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedDisplay(): void {
|
||||
if (!this.options.allowMultiple || !this.selectedContainer) return;
|
||||
|
||||
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
|
||||
.map(item => `
|
||||
<span class="autocomplete-tag" style="
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
">
|
||||
${this.escapeHtml(item)}
|
||||
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}" style="
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
">×</button>
|
||||
</span>
|
||||
`)
|
||||
.join('');
|
||||
|
||||
// Bind remove events
|
||||
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.removeItem((btn as HTMLElement).getAttribute('data-item')!);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateInputValue(): void {
|
||||
if (this.options.allowMultiple && this.options.hiddenInput) {
|
||||
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator || ', ');
|
||||
}
|
||||
}
|
||||
|
||||
showDropdown(): void {
|
||||
this.dropdown.style.display = 'block';
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
hideDropdown(): void {
|
||||
this.dropdown.style.display = 'none';
|
||||
this.isOpen = false;
|
||||
this.selectedIndex = -1;
|
||||
}
|
||||
|
||||
escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
setDataSource(newDataSource: any[]): void {
|
||||
this.dataSource = newDataSource;
|
||||
}
|
||||
|
||||
getSelectedItems(): string[] {
|
||||
return Array.from(this.selectedItems);
|
||||
}
|
||||
|
||||
setSelectedItems(items: string[]): void {
|
||||
this.selectedItems = new Set(items);
|
||||
if (this.options.allowMultiple) {
|
||||
this.updateSelectedDisplay();
|
||||
this.updateInputValue();
|
||||
}
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.dropdown && this.dropdown.parentNode) {
|
||||
this.dropdown.parentNode.removeChild(this.dropdown);
|
||||
}
|
||||
if (this.selectedContainer && this.selectedContainer.parentNode) {
|
||||
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,22 +5,23 @@ export interface ContributionData {
|
||||
type: 'add' | 'edit';
|
||||
tool: {
|
||||
name: string;
|
||||
icon?: string;
|
||||
icon?: string | null;
|
||||
type: 'software' | 'method' | 'concept';
|
||||
description: string;
|
||||
domains: string[];
|
||||
phases: string[];
|
||||
platforms: string[];
|
||||
skillLevel: string;
|
||||
accessType?: string;
|
||||
accessType?: string | null;
|
||||
url: string;
|
||||
projectUrl?: string;
|
||||
license?: string;
|
||||
knowledgebase?: boolean;
|
||||
'domain-agnostic-software'?: string[];
|
||||
related_concepts?: string[];
|
||||
projectUrl?: string | null;
|
||||
license?: string | null;
|
||||
knowledgebase?: boolean | null;
|
||||
'domain-agnostic-software'?: string[] | null;
|
||||
related_concepts?: string[] | null;
|
||||
related_software?: string[] | null;
|
||||
tags: string[];
|
||||
statusUrl?: string;
|
||||
statusUrl?: string | null;
|
||||
};
|
||||
metadata: {
|
||||
submitter: string;
|
||||
@ -134,6 +135,7 @@ export class GitContributionManager {
|
||||
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
|
||||
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
|
||||
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
|
||||
if (tool.related_software?.length) cleanTool.related_software = tool.related_software;
|
||||
if (tool.tags?.length) cleanTool.tags = tool.tags;
|
||||
if (tool['domain-agnostic-software']?.length) {
|
||||
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
|
||||
@ -272,6 +274,8 @@ ${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(',
|
||||
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
|
||||
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
|
||||
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
|
||||
${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
|
||||
${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''}
|
||||
|
||||
${data.metadata.reason ? `### Reason
|
||||
${data.metadata.reason}
|
||||
@ -299,9 +303,6 @@ ${data.metadata.contact}
|
||||
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Header */
|
||||
/* ------------------------------------------------------------------ */
|
||||
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
|
||||
sections.push('');
|
||||
sections.push(`**Submitted by:** ${data.submitter}`);
|
||||
@ -310,18 +311,12 @@ ${data.metadata.contact}
|
||||
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
|
||||
sections.push('');
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Description */
|
||||
/* ------------------------------------------------------------------ */
|
||||
if (data.description) {
|
||||
sections.push('### Description');
|
||||
sections.push(data.description);
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Content */
|
||||
/* ------------------------------------------------------------------ */
|
||||
if (data.content) {
|
||||
sections.push('### Article Content');
|
||||
sections.push('```markdown');
|
||||
@ -330,18 +325,12 @@ ${data.metadata.contact}
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* External resources */
|
||||
/* ------------------------------------------------------------------ */
|
||||
if (data.externalLink) {
|
||||
sections.push('### External Resource');
|
||||
sections.push(`- [External Documentation](${data.externalLink})`);
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Uploaded files */
|
||||
/* ------------------------------------------------------------------ */
|
||||
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
|
||||
sections.push('### Uploaded Files');
|
||||
data.uploadedFiles.forEach((file) => {
|
||||
@ -359,9 +348,6 @@ ${data.metadata.contact}
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Categories & Tags */
|
||||
/* ------------------------------------------------------------------ */
|
||||
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
|
||||
const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
|
||||
|
||||
@ -372,18 +358,12 @@ ${data.metadata.contact}
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Reason */
|
||||
/* ------------------------------------------------------------------ */
|
||||
if (data.reason) {
|
||||
sections.push('### Reason for Contribution');
|
||||
sections.push(data.reason);
|
||||
sections.push('');
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Footer */
|
||||
/* ------------------------------------------------------------------ */
|
||||
sections.push('### For Maintainers');
|
||||
sections.push('1. Review the content for quality and accuracy');
|
||||
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');
|
||||
|
Loading…
x
Reference in New Issue
Block a user