fix contrib system
This commit is contained in:
parent
3d42fcef79
commit
3c55742dfa
@ -37,7 +37,7 @@ const ContributionRequestSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
tool: ContributionToolSchema,
|
tool: ContributionToolSchema,
|
||||||
metadata: z.object({
|
metadata: z.object({
|
||||||
reason: z.string().max(500, 'Reason too long').optional()
|
reason: z.string().transform(val => val.trim() === '' ? undefined : val).optional()
|
||||||
}).optional().default({})
|
}).optional().default({})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -412,6 +412,8 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Contribute New Tool';
|
|||||||
domainAgnosticSoftware,
|
domainAgnosticSoftware,
|
||||||
existingConcepts: existingTools.filter(t => t.type === 'concept')
|
existingConcepts: existingTools.filter(t => t.type === 'concept')
|
||||||
}}>
|
}}>
|
||||||
|
// REPLACE the JavaScript section at the bottom of tool.astro with this:
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const form = document.getElementById('contribution-form');
|
const form = document.getElementById('contribution-form');
|
||||||
const typeSelect = document.getElementById('tool-type');
|
const typeSelect = document.getElementById('tool-type');
|
||||||
@ -447,23 +449,28 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const descriptionCount = document.getElementById('description-count');
|
const descriptionCount = document.getElementById('description-count');
|
||||||
const reasonCount = document.getElementById('reason-count');
|
const reasonCount = document.getElementById('reason-count');
|
||||||
|
|
||||||
descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000));
|
if (descriptionTextarea && descriptionCount) {
|
||||||
reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500));
|
descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000));
|
||||||
|
updateCharacterCounter(descriptionTextarea, descriptionCount, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
// Initial counter update
|
if (reasonTextarea && reasonCount) {
|
||||||
updateCharacterCounter(descriptionTextarea, descriptionCount, 1000);
|
reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500));
|
||||||
updateCharacterCounter(reasonTextarea, reasonCount, 500);
|
updateCharacterCounter(reasonTextarea, reasonCount, 500);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle type-specific field visibility
|
// Handle type-specific field visibility
|
||||||
function updateFieldVisibility() {
|
function updateFieldVisibility() {
|
||||||
const selectedType = typeSelect.value;
|
const selectedType = typeSelect.value;
|
||||||
|
|
||||||
// Hide all type-specific fields
|
// Hide all type-specific fields
|
||||||
softwareFields.style.display = 'none';
|
if (softwareFields) softwareFields.style.display = 'none';
|
||||||
relatedConceptsField.style.display = 'none';
|
if (relatedConceptsField) relatedConceptsField.style.display = 'none';
|
||||||
|
|
||||||
// Show project URL for software only
|
// Show project URL for software only
|
||||||
projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none';
|
if (projectUrlField) {
|
||||||
|
projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Handle required fields
|
// Handle required fields
|
||||||
const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]');
|
const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]');
|
||||||
@ -471,34 +478,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (selectedType === 'software') {
|
if (selectedType === 'software') {
|
||||||
// Show software-specific fields
|
// Show software-specific fields
|
||||||
softwareFields.style.display = 'block';
|
if (softwareFields) softwareFields.style.display = 'block';
|
||||||
relatedConceptsField.style.display = 'block';
|
if (relatedConceptsField) relatedConceptsField.style.display = 'block';
|
||||||
|
|
||||||
// Make platforms and license required
|
// Make platforms and license required
|
||||||
platformsRequired.style.display = 'inline';
|
if (platformsRequired) platformsRequired.style.display = 'inline';
|
||||||
licenseRequired.style.display = 'inline';
|
if (licenseRequired) licenseRequired.style.display = 'inline';
|
||||||
platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required'));
|
platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required'));
|
||||||
licenseSelect.setAttribute('required', 'required');
|
if (licenseSelect) licenseSelect.setAttribute('required', 'required');
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Hide required indicators and remove requirements
|
// Hide required indicators and remove requirements
|
||||||
platformsRequired.style.display = 'none';
|
if (platformsRequired) platformsRequired.style.display = 'none';
|
||||||
licenseRequired.style.display = 'none';
|
if (licenseRequired) licenseRequired.style.display = 'none';
|
||||||
platformsCheckboxes.forEach(cb => cb.removeAttribute('required'));
|
platformsCheckboxes.forEach(cb => cb.removeAttribute('required'));
|
||||||
licenseSelect.removeAttribute('required');
|
if (licenseSelect) licenseSelect.removeAttribute('required');
|
||||||
|
|
||||||
// Show related concepts for methods
|
// Show related concepts for methods
|
||||||
if (selectedType === 'method') {
|
if (selectedType === 'method' && relatedConceptsField) {
|
||||||
relatedConceptsField.style.display = 'block';
|
relatedConceptsField.style.display = 'block';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update YAML preview
|
// Update YAML preview
|
||||||
updateYAMLPreview();
|
if (typeof updateYAMLPreview === 'function') {
|
||||||
|
updateYAMLPreview();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate YAML preview
|
// Generate YAML preview
|
||||||
function updateYAMLPreview() {
|
function updateYAMLPreview() {
|
||||||
|
if (!yamlPreview) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const toolData = {
|
const toolData = {
|
||||||
@ -533,62 +544,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Handle related concepts
|
// Handle related concepts
|
||||||
if (toolData.type !== 'concept') {
|
if (toolData.type !== 'concept') {
|
||||||
toolData.related_concepts = formData.getAll('relatedConcepts') || null;
|
const relatedConcepts = formData.getAll('relatedConcepts');
|
||||||
|
toolData.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to YAML-like format for preview
|
// Generate YAML
|
||||||
let yamlContent = `- name: "${toolData.name}"\n`;
|
yamlPreview.textContent = `name: "${toolData.name}"
|
||||||
if (toolData.icon) yamlContent += ` icon: "${toolData.icon}"\n`;
|
${toolData.icon ? `icon: "${toolData.icon}"` : '# icon: "📦"'}
|
||||||
yamlContent += ` type: ${toolData.type}\n`;
|
type: ${toolData.type}
|
||||||
yamlContent += ` description: >\n ${toolData.description}\n`;
|
description: "${toolData.description}"
|
||||||
if (toolData.domains.length > 0) {
|
domains: [${toolData.domains.map(d => `"${d}"`).join(', ')}]
|
||||||
yamlContent += ` domains:\n${toolData.domains.map(d => ` - ${d}`).join('\n')}\n`;
|
phases: [${toolData.phases.map(p => `"${p}"`).join(', ')}]
|
||||||
}
|
skillLevel: ${toolData.skillLevel}
|
||||||
if (toolData.phases.length > 0) {
|
url: "${toolData.url}"${toolData.platforms.length > 0 ? `
|
||||||
yamlContent += ` phases:\n${toolData.phases.map(p => ` - ${p}`).join('\n')}\n`;
|
platforms: [${toolData.platforms.map(p => `"${p}"`).join(', ')}]` : ''}${toolData.license ? `
|
||||||
}
|
license: "${toolData.license}"` : ''}${toolData.accessType ? `
|
||||||
if (toolData.platforms.length > 0) {
|
accessType: ${toolData.accessType}` : ''}${toolData.projectUrl ? `
|
||||||
yamlContent += ` platforms:\n${toolData.platforms.map(p => ` - ${p}`).join('\n')}\n`;
|
projectUrl: "${toolData.projectUrl}"` : ''}${toolData.knowledgebase ? `
|
||||||
}
|
knowledgebase: true` : ''}${toolData.tags.length > 0 ? `
|
||||||
yamlContent += ` skillLevel: ${toolData.skillLevel}\n`;
|
tags: [${toolData.tags.map(t => `"${t}"`).join(', ')}]` : ''}${toolData.related_concepts ? `
|
||||||
if (toolData.accessType) yamlContent += ` accessType: ${toolData.accessType}\n`;
|
related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]` : ''}`;
|
||||||
yamlContent += ` url: ${toolData.url}\n`;
|
|
||||||
if (toolData.projectUrl) yamlContent += ` projectUrl: ${toolData.projectUrl}\n`;
|
|
||||||
if (toolData.license) yamlContent += ` license: ${toolData.license}\n`;
|
|
||||||
if (toolData.knowledgebase) yamlContent += ` knowledgebase: ${toolData.knowledgebase}\n`;
|
|
||||||
if (toolData.related_concepts && toolData.related_concepts.length > 0) {
|
|
||||||
yamlContent += ` related_concepts:\n${toolData.related_concepts.map(c => ` - ${c}`).join('\n')}\n`;
|
|
||||||
}
|
|
||||||
if (toolData.tags.length > 0) {
|
|
||||||
yamlContent += ` tags:\n${toolData.tags.map(t => ` - ${t}`).join('\n')}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlPreview.textContent = yamlContent;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
yamlPreview.textContent = `# Error generating preview: ${error.message}`;
|
yamlPreview.textContent = '# Error generating preview';
|
||||||
|
console.error('YAML preview error:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form validation
|
// Form validation
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const selectedType = typeSelect.value;
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!nameInput.value.trim()) errors.push('Name is required');
|
if (!formData.get('name')?.trim()) {
|
||||||
if (!descriptionTextarea.value.trim() || descriptionTextarea.value.length < 10) {
|
errors.push('Tool name is required');
|
||||||
errors.push('Description must be at least 10 characters');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedType = typeSelect.value;
|
if (!formData.get('description')?.trim()) {
|
||||||
|
errors.push('Description is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.get('url')?.trim()) {
|
||||||
|
errors.push('URL is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.get('skillLevel')) {
|
||||||
|
errors.push('Skill level is required');
|
||||||
|
}
|
||||||
|
|
||||||
// Type-specific validation
|
// Type-specific validation
|
||||||
if (selectedType === 'software') {
|
if (selectedType === 'software') {
|
||||||
const platforms = new FormData(form).getAll('platforms');
|
const platforms = formData.getAll('platforms');
|
||||||
if (platforms.length === 0) {
|
if (platforms.length === 0) {
|
||||||
errors.push('At least one platform is required for software');
|
errors.push('At least one platform is required for software');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.getElementById('tool-license').value.trim()) {
|
if (!document.getElementById('tool-license')?.value?.trim()) {
|
||||||
errors.push('License is required for software');
|
errors.push('License is required for software');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -597,124 +610,171 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
typeSelect.addEventListener('change', updateFieldVisibility);
|
if (typeSelect) {
|
||||||
refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
|
typeSelect.addEventListener('change', updateFieldVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshPreviewBtn) {
|
||||||
|
refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
|
||||||
|
}
|
||||||
|
|
||||||
// Update preview on form changes
|
// Update preview on form changes
|
||||||
form.addEventListener('input', debounce(updateYAMLPreview, 500));
|
if (form) {
|
||||||
form.addEventListener('change', updateYAMLPreview);
|
form.addEventListener('input', debounce(updateYAMLPreview, 500));
|
||||||
|
form.addEventListener('change', updateYAMLPreview);
|
||||||
|
}
|
||||||
|
|
||||||
// Form submission
|
// Form submission
|
||||||
form.addEventListener('submit', async (e) => {
|
if (form) {
|
||||||
e.preventDefault();
|
form.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
const errors = validateForm();
|
|
||||||
if (errors.length > 0) {
|
|
||||||
alert('Please fix the following errors:\n' + errors.join('\n'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
submitBtn.disabled = true;
|
|
||||||
submitText.textContent = isEdit ? 'Updating...' : 'Submitting...';
|
|
||||||
submitSpinner.style.display = 'inline-block';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
// Prepare submission data
|
const errors = validateForm();
|
||||||
const submissionData = {
|
if (errors.length > 0) {
|
||||||
action: isEdit ? 'edit' : 'add',
|
alert('Please fix the following errors:\n' + errors.join('\n'));
|
||||||
tool: {
|
return;
|
||||||
name: formData.get('name'),
|
|
||||||
icon: formData.get('icon') || null,
|
|
||||||
type: formData.get('type'),
|
|
||||||
description: formData.get('description'),
|
|
||||||
domains: formData.getAll('domains'),
|
|
||||||
phases: formData.getAll('phases'),
|
|
||||||
skillLevel: formData.get('skillLevel'),
|
|
||||||
url: formData.get('url'),
|
|
||||||
tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : []
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
reason: formData.get('reason') || null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add type-specific fields
|
|
||||||
if (submissionData.tool.type === 'software') {
|
|
||||||
submissionData.tool.platforms = formData.getAll('platforms');
|
|
||||||
submissionData.tool.license = formData.get('license').trim();
|
|
||||||
submissionData.tool.accessType = formData.get('accessType');
|
|
||||||
submissionData.tool.projectUrl = formData.get('projectUrl') || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add optional fields
|
// Show loading state
|
||||||
submissionData.tool.knowledgebase = formData.has('knowledgebase') || null;
|
if (submitBtn) submitBtn.disabled = true;
|
||||||
|
if (submitText) submitText.textContent = isEdit ? 'Updating...' : 'Submitting...';
|
||||||
if (submissionData.tool.type !== 'concept') {
|
if (submitSpinner) submitSpinner.style.display = 'inline-block';
|
||||||
const relatedConcepts = formData.getAll('relatedConcepts');
|
|
||||||
submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch('/api/contribute/tool', {
|
try {
|
||||||
method: 'POST',
|
const formData = new FormData(form);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(submissionData)
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// Show success modal
|
|
||||||
document.getElementById('success-message').textContent =
|
|
||||||
`Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`;
|
|
||||||
|
|
||||||
if (result.prUrl) {
|
// FIXED: Prepare submission data with proper metadata.reason handling
|
||||||
const prLink = document.getElementById('pr-link');
|
const submissionData = {
|
||||||
prLink.href = result.prUrl;
|
action: isEdit ? 'edit' : 'add',
|
||||||
prLink.style.display = 'inline-flex';
|
tool: {
|
||||||
|
name: formData.get('name'),
|
||||||
|
icon: formData.get('icon') || null,
|
||||||
|
type: formData.get('type'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
domains: formData.getAll('domains'),
|
||||||
|
phases: formData.getAll('phases'),
|
||||||
|
skillLevel: formData.get('skillLevel'),
|
||||||
|
url: formData.get('url'),
|
||||||
|
tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : []
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
// FIXED: Always provide a string, never null
|
||||||
|
reason: formData.get('reason')?.trim() || ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add type-specific fields
|
||||||
|
if (submissionData.tool.type === 'software') {
|
||||||
|
submissionData.tool.platforms = formData.getAll('platforms');
|
||||||
|
submissionData.tool.license = formData.get('license')?.trim() || null;
|
||||||
|
submissionData.tool.accessType = formData.get('accessType') || null;
|
||||||
|
submissionData.tool.projectUrl = formData.get('projectUrl')?.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add optional fields
|
||||||
|
submissionData.tool.knowledgebase = formData.has('knowledgebase') || null;
|
||||||
|
|
||||||
|
if (submissionData.tool.type !== 'concept') {
|
||||||
|
const relatedConcepts = formData.getAll('relatedConcepts');
|
||||||
|
submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Submitting:', submissionData); // Debug log
|
||||||
|
|
||||||
|
const response = await fetch('/api/contribute/tool', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(submissionData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Show success modal
|
||||||
|
if (successModal) {
|
||||||
|
const successMessage = document.getElementById('success-message');
|
||||||
|
if (successMessage) {
|
||||||
|
successMessage.textContent = `Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.prUrl) {
|
||||||
|
const prLink = document.getElementById('pr-link');
|
||||||
|
if (prLink) {
|
||||||
|
prLink.href = result.prUrl;
|
||||||
|
prLink.style.display = 'inline-flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let errorMessage = result.error || 'Submission failed';
|
||||||
|
if (result.details && Array.isArray(result.details)) {
|
||||||
|
errorMessage += '\n\nDetails:\n' + result.details.join('\n');
|
||||||
|
}
|
||||||
|
alert(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Submission error:', error);
|
||||||
|
alert('An error occurred while submitting your contribution. Please try again.');
|
||||||
|
} finally {
|
||||||
|
// Reset loading state
|
||||||
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
|
if (submitText) submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution';
|
||||||
|
if (submitSpinner) submitSpinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Initialize form with proper order
|
||||||
|
function initializeForm() {
|
||||||
|
if (isEdit && editTool) {
|
||||||
|
console.log('Initializing edit form for:', editTool.name);
|
||||||
|
|
||||||
|
// Set basic fields first
|
||||||
|
if (typeSelect) {
|
||||||
|
typeSelect.value = editTool.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update field visibility FIRST
|
||||||
|
updateFieldVisibility();
|
||||||
|
|
||||||
|
// THEN set the platform checkboxes and access type after a brief delay
|
||||||
|
setTimeout(() => {
|
||||||
|
// FIXED: Set platform checkboxes with more specific selector
|
||||||
|
if (editTool.platforms) {
|
||||||
|
editTool.platforms.forEach(platform => {
|
||||||
|
const checkbox = document.querySelector(`input[name="platforms"][value="${platform}"]`);
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.checked = true;
|
||||||
|
console.log('Set platform checkbox:', platform);
|
||||||
|
} else {
|
||||||
|
console.warn('Platform checkbox not found:', platform);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
successModal.style.display = 'flex';
|
// FIXED: Set access type value
|
||||||
} else {
|
if (editTool.accessType) {
|
||||||
let errorMessage = result.error || 'Submission failed';
|
const accessTypeSelect = document.getElementById('access-type');
|
||||||
if (result.details && Array.isArray(result.details)) {
|
if (accessTypeSelect) {
|
||||||
errorMessage += '\n\nDetails:\n' + result.details.join('\n');
|
accessTypeSelect.value = editTool.accessType;
|
||||||
|
console.log('Set access type:', editTool.accessType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
alert(errorMessage);
|
|
||||||
}
|
// Update YAML preview after all values are set
|
||||||
} catch (error) {
|
updateYAMLPreview();
|
||||||
console.error('Submission error:', error);
|
}, 100);
|
||||||
alert('An error occurred while submitting your contribution. Please try again.');
|
} else {
|
||||||
} finally {
|
updateFieldVisibility();
|
||||||
// Reset loading state
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution';
|
|
||||||
submitSpinner.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Initialize form
|
// Initialize form
|
||||||
if (isEdit && editTool) {
|
initializeForm();
|
||||||
// Pre-fill edit form
|
|
||||||
typeSelect.value = editTool.type;
|
|
||||||
updateFieldVisibility();
|
|
||||||
|
|
||||||
// Set checkboxes for platforms
|
|
||||||
if (editTool.platforms) {
|
|
||||||
editTool.platforms.forEach(platform => {
|
|
||||||
const checkbox = document.querySelector(`input[value="${platform}"]`);
|
|
||||||
if (checkbox) checkbox.checked = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateYAMLPreview();
|
|
||||||
} else {
|
|
||||||
updateFieldVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce utility
|
// Debounce utility
|
||||||
function debounce(func, wait) {
|
function debounce(func, wait) {
|
||||||
|
@ -285,120 +285,152 @@ export class GitContributionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Original tool contribution methods (unchanged)
|
|
||||||
|
|
||||||
async submitContribution(data: ContributionData): Promise<GitOperationResult> {
|
private generateToolYAML(tool: any): string {
|
||||||
const branchName = `tool-${data.type}-${Date.now()}`;
|
// Clean up the tool object - remove null/undefined values
|
||||||
|
const cleanTool: any = {
|
||||||
|
name: tool.name,
|
||||||
|
type: tool.type,
|
||||||
|
description: tool.description,
|
||||||
|
domains: tool.domains || [],
|
||||||
|
phases: tool.phases || [],
|
||||||
|
skillLevel: tool.skillLevel,
|
||||||
|
url: tool.url
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add optional fields only if they have values
|
||||||
|
if (tool.icon) cleanTool.icon = tool.icon;
|
||||||
|
if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms;
|
||||||
|
if (tool.license) cleanTool.license = tool.license;
|
||||||
|
if (tool.accessType) cleanTool.accessType = tool.accessType;
|
||||||
|
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
|
||||||
|
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
|
||||||
|
if (tool.related_concepts && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts;
|
||||||
|
if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags;
|
||||||
|
|
||||||
|
// Generate clean YAML
|
||||||
|
return dump(cleanTool, {
|
||||||
|
lineWidth: -1,
|
||||||
|
noRefs: true,
|
||||||
|
quotingType: '"',
|
||||||
|
forceQuotes: false,
|
||||||
|
indent: 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitContribution(data: ContributionData): Promise<GitOperationResult> {
|
||||||
|
const branchName = `tool-${data.type}-${Date.now()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create branch
|
||||||
|
await this.createBranch(branchName);
|
||||||
|
|
||||||
|
// FIXED: Don't modify tools.yaml at all - just create the tool data file
|
||||||
|
const toolYaml = this.generateToolYAML(data.tool);
|
||||||
|
const contributionFileName = `contribution-${data.type}-${data.tool.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.yaml`;
|
||||||
|
|
||||||
|
// Create contributions directory if it doesn't exist and write the tool data
|
||||||
try {
|
try {
|
||||||
// Create branch
|
await this.readFile('contributions/.gitkeep');
|
||||||
await this.createBranch(branchName);
|
} catch {
|
||||||
|
// Directory doesn't exist, create it
|
||||||
// Load current tools.yaml
|
await this.writeFile('contributions/.gitkeep', '# Contribution files directory\n');
|
||||||
const toolsYamlPath = 'src/data/tools.yaml';
|
}
|
||||||
const content = await this.readFile(toolsYamlPath);
|
|
||||||
const yamlData = load(content) as any;
|
// Write the tool data as a separate file for maintainers to review
|
||||||
|
await this.writeFile(`contributions/${contributionFileName}`, toolYaml);
|
||||||
if (!yamlData.tools) {
|
|
||||||
yamlData.tools = [];
|
// Commit changes
|
||||||
}
|
const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
|
||||||
|
|
||||||
// Apply changes
|
|
||||||
if (data.type === 'add') {
|
|
||||||
// Check if tool already exists
|
|
||||||
const existing = yamlData.tools.find((t: any) => t.name === data.tool.name);
|
|
||||||
if (existing) {
|
|
||||||
throw new Error(`Tool "${data.tool.name}" already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlData.tools.push(data.tool);
|
|
||||||
} else if (data.type === 'edit') {
|
|
||||||
const index = yamlData.tools.findIndex((t: any) => t.name === data.tool.name);
|
|
||||||
if (index === -1) {
|
|
||||||
throw new Error(`Tool "${data.tool.name}" not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
yamlData.tools[index] = { ...yamlData.tools[index], ...data.tool };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort tools alphabetically
|
|
||||||
yamlData.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
// Generate updated YAML
|
|
||||||
const updatedYaml = dump(yamlData, {
|
|
||||||
lineWidth: -1,
|
|
||||||
noRefs: true,
|
|
||||||
quotingType: '"',
|
|
||||||
forceQuotes: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Write updated file
|
|
||||||
await this.writeFile(toolsYamlPath, updatedYaml);
|
|
||||||
|
|
||||||
// Commit changes
|
|
||||||
const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
|
|
||||||
|
|
||||||
Contributed by: ${data.metadata.submitter}
|
Contributed by: ${data.metadata.submitter}
|
||||||
Type: ${data.tool.type}
|
Type: ${data.tool.type}
|
||||||
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
|
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}
|
||||||
|
|
||||||
await this.commitChanges(commitMessage);
|
|
||||||
|
|
||||||
// Push branch
|
|
||||||
await this.pushBranch(branchName);
|
|
||||||
|
|
||||||
// Create pull request
|
|
||||||
const prUrl = await this.createPullRequest(
|
|
||||||
branchName,
|
|
||||||
`${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
|
|
||||||
this.generatePRDescription(data)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Tool contribution submitted successfully`,
|
|
||||||
prUrl,
|
|
||||||
branchName
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Cleanup on failure
|
|
||||||
try {
|
|
||||||
await this.deleteBranch(branchName);
|
|
||||||
} catch (cleanupError) {
|
|
||||||
console.error('Failed to cleanup branch:', cleanupError);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generatePRDescription(data: ContributionData): string {
|
This contribution contains the raw tool data for manual review and integration.`;
|
||||||
|
|
||||||
|
await this.commitChanges(commitMessage);
|
||||||
|
|
||||||
|
// Push branch
|
||||||
|
await this.pushBranch(branchName);
|
||||||
|
|
||||||
|
// Create pull request with enhanced description
|
||||||
|
const prUrl = await this.createPullRequest(
|
||||||
|
branchName,
|
||||||
|
`${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
|
||||||
|
this.generateEnhancedPRDescription(data, toolYaml)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Tool contribution submitted successfully`,
|
||||||
|
prUrl,
|
||||||
|
branchName
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Cleanup on failure
|
||||||
|
try {
|
||||||
|
await this.deleteBranch(branchName);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Failed to cleanup branch:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string {
|
||||||
return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name}
|
return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name}
|
||||||
|
|
||||||
**Type:** ${data.tool.type}
|
**Type:** ${data.tool.type}
|
||||||
**Submitted by:** ${data.metadata.submitter}
|
**Submitted by:** ${data.metadata.submitter}
|
||||||
|
**Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
|
||||||
|
|
||||||
### Tool Details
|
### Tool Details
|
||||||
- **Description:** ${data.tool.description}
|
- **Name:** ${data.tool.name}
|
||||||
- **Domains:** ${data.tool.domains.join(', ')}
|
- **Description:** ${data.tool.description}
|
||||||
- **Phases:** ${data.tool.phases.join(', ')}
|
- **URL:** ${data.tool.url}
|
||||||
- **Skill Level:** ${data.tool.skillLevel}
|
- **Skill Level:** ${data.tool.skillLevel}
|
||||||
- **License:** ${data.tool.license || 'Not specified'}
|
${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
|
||||||
- **URL:** ${data.tool.url}
|
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
|
||||||
|
${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''}
|
||||||
|
${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''}
|
||||||
|
- **Domains:** ${data.tool.domains.join(', ')}
|
||||||
|
- **Phases:** ${data.tool.phases.join(', ')}
|
||||||
|
${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''}
|
||||||
|
${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
|
||||||
|
|
||||||
${data.metadata.reason ? `### Reason for Contribution\n${data.metadata.reason}` : ''}
|
${data.metadata.reason ? `### Reason for Contribution
|
||||||
|
${data.metadata.reason}
|
||||||
|
|
||||||
### Review Checklist
|
` : ''}### Raw Tool Data (Copy & Paste Ready)
|
||||||
- [ ] Tool information is accurate and complete
|
|
||||||
- [ ] Description is clear and informative
|
|
||||||
- [ ] Domains and phases are correctly assigned
|
|
||||||
- [ ] Tags are relevant and consistent
|
|
||||||
- [ ] License information is correct
|
|
||||||
- [ ] URLs are valid and accessible
|
|
||||||
|
|
||||||
---
|
\`\`\`yaml
|
||||||
*This contribution was submitted via the CC24-Hub web interface.*`;
|
${toolYaml}\`\`\`
|
||||||
|
|
||||||
|
### For Maintainers
|
||||||
|
|
||||||
|
**To add this tool to tools.yaml:**
|
||||||
|
1. Copy the YAML data above
|
||||||
|
2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'}
|
||||||
|
3. Verify all fields are correct
|
||||||
|
4. Test that the tool displays properly
|
||||||
|
5. Close this PR
|
||||||
|
|
||||||
|
### Review Checklist
|
||||||
|
- [ ] Tool information is accurate and complete
|
||||||
|
- [ ] Description is clear and informative
|
||||||
|
- [ ] Domains and phases are correctly assigned
|
||||||
|
- [ ] Tags are relevant and consistent with existing tools
|
||||||
|
- [ ] License information is correct (for software)
|
||||||
|
- [ ] URLs are valid and accessible
|
||||||
|
- [ ] No duplicate tool entries
|
||||||
|
- [ ] YAML syntax is valid
|
||||||
|
|
||||||
|
---
|
||||||
|
*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> {
|
async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user