fix contrib form

This commit is contained in:
overcuriousity 2025-07-25 17:34:42 +02:00
parent 78dc47d218
commit d80a4d85eb
5 changed files with 582 additions and 233 deletions

View File

@ -175,17 +175,6 @@ export const POST: APIRoute = async ({ request }) => {
const errorMessages = error.errors.map(err => const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}` `${err.path.join('.')}: ${err.message}`
); );
// BEFORE: Manual validation error response (7 lines)
// return new Response(JSON.stringify({
// success: false,
// error: 'Validation failed',
// details: errorMessages
// }), {
// status: 400,
// headers: { 'Content-Type': 'application/json' }
// });
// AFTER: Single line with consolidated helper
return apiError.validation('Validation failed', errorMessages); return apiError.validation('Validation failed', errorMessages);
} }
@ -208,46 +197,35 @@ export const POST: APIRoute = async ({ request }) => {
} }
}; };
// Submit contribution via Git // CRITICAL FIX: Enhanced error handling for Git operations
const gitManager = new GitContributionManager(); try {
const result = await gitManager.submitContribution(contributionData); const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData);
if (result.success) { if (result.success) {
// Log successful contribution // Log successful contribution
console.log(`[CONTRIBUTION] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`); console.log(`[CONTRIBUTION SUCCESS] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`);
// ENSURE proper success response
return apiResponse.created({
success: true,
message: result.message,
prUrl: result.prUrl,
branchName: result.branchName
});
} else {
// Log failed contribution
console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`);
return apiServerError.internal(`Contribution failed: ${result.message}`);
}
} catch (gitError) {
// CRITICAL: Handle Git operation errors properly
console.error(`[GIT ERROR] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}:`, gitError);
// BEFORE: Manual success response (7 lines) // Return proper error response
// return new Response(JSON.stringify({ const errorMessage = gitError instanceof Error ? gitError.message : 'Git operation failed';
// success: true, return apiServerError.internal(`Git operation failed: ${errorMessage}`);
// message: result.message,
// prUrl: result.prUrl,
// branchName: result.branchName
// }), {
// status: 200,
// headers: { 'Content-Type': 'application/json' }
// });
// AFTER: Single line with consolidated helper
return apiResponse.created({
message: result.message,
prUrl: result.prUrl,
branchName: result.branchName
});
} else {
// Log failed contribution
console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`);
// BEFORE: Manual error response (7 lines)
// return new Response(JSON.stringify({
// success: false,
// error: result.message
// }), {
// status: 500,
// headers: { 'Content-Type': 'application/json' }
// });
// AFTER: Single line with consolidated helper
return apiServerError.internal(`Contribution failed: ${result.message}`);
} }
}, 'Contribution processing failed'); }, 'Contribution processing failed');

View File

@ -50,37 +50,68 @@ const { authenticated, userEmail, userId } = authResult;
" "
> >
<!-- Tools, Methods & Concepts --> <!-- src/pages/contribute/index.astro - Replace the Tools/Methods/Concepts card -->
<div class="card"
style="padding: 2rem; border-left: 4px solid var(--color-primary); cursor: pointer; transition: var(--transition-fast); <!-- Tools, Methods & Concepts - IMPROVED UX -->
display:flex; flex-direction:column;" <div class="card"
onclick="window.location.href='/contribute/tool'"> style="padding: 2rem; border-left: 4px solid var(--color-primary); transition: var(--transition-fast);
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;"> display:flex; flex-direction:column;">
<div style="width: 48px; height: 48px; background-color: var(--color-primary); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center;"> <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <div style="width: 48px; height: 48px; background-color: var(--color-primary); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center;">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>
</div>
<h3 style="margin: 0; color: var(--color-primary); font-size: 1.25rem;">Software, Methoden oder Konzepte</h3>
</div>
<p style="margin-bottom: 1.5rem; line-height: 1.6;">
Ergänzt Software/Tools, forensische Methoden und relevante Konzepte zu unserer Datenbank.
Füllt einfach ein kurzes Formular aus!
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem;">
<span class="badge" style="background-color: var(--color-primary); color: white;">Software/Tools</span>
<span class="badge" style="background-color: var(--color-method); color: white;">Methoden</span>
<span class="badge" style="background-color: var(--color-concept); color: white;">Konzepte</span>
</div>
<div style="margin-top:auto; display:flex; flex-direction: column; gap:1rem;">
<a href="/contribute/tool" class="btn btn-primary" style="width: 100%;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Neuer Eintrag
</a>
<!-- IMPROVED: Clear guidance instead of confusing button -->
<div style="background-color: var(--color-bg-secondary); padding: 1.25rem; border-radius: 0.5rem; border-left: 3px solid var(--color-accent);">
<div style="display: flex; align-items: start; gap: 0.75rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" style="flex-shrink: 0; margin-top: 0.125rem;">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg> </svg>
<div>
<h4 style="margin: 0 0 0.5rem 0; color: var(--color-accent); font-size: 0.9375rem;">Existierenden Eintrag bearbeiten</h4>
<p style="margin: 0; font-size: 0.875rem; line-height: 1.5; color: var(--color-text-secondary);">
Suchen Sie das Tool/Methode/Konzept auf der <a href="/" style="color: var(--color-primary); text-decoration: underline;">Hauptseite</a>,
öffnen Sie die Details und klicken Sie den <strong style="color: var(--color-text);">Edit</strong>-Button.
</p>
<div style="margin-top: 0.75rem;">
<a href="/" class="btn btn-secondary" style="font-size: 0.8125rem; padding: 0.5rem 1rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
Zur Hauptseite
</a>
</div>
</div>
</div> </div>
<h3 style="margin: 0; color: var(--color-primary); font-size: 1.25rem;">Software, Methoden oder Konzepte</h3>
</div>
<p style="margin-bottom: 1.5rem; line-height: 1.6;">
Ergänzt Software/Tools, forensische Methoden und relevante Konzepte zu unserer Datenbank.
Füllt einfach ein kurzes Formular aus!
</p>
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem;">
<span class="badge" style="background-color: var(--color-primary); color: white;">Software/Tools</span>
<span class="badge" style="background-color: var(--color-method); color: white;">Methoden</span>
<span class="badge" style="background-color: var(--color-concept); color: white;">Konzepte</span>
</div>
<div style="margin-top:auto; display:flex; gap:1rem;">
<a href="/contribute/tool" class="btn btn-primary" style="flex: 1;">Neuer Eintrag</a>
<a href="/contribute/tool?mode=browse" class="btn btn-secondary" style="flex: 1;">Existierenden Bearbeiten</a>
</div> </div>
</div> </div>
</div>
<!-- Knowledgebase Articles --> <!-- Knowledgebase Articles -->
<div class="card" <div class="card"
style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast); style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast);

View File

@ -71,7 +71,7 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
</div> </div>
<!-- Basic Information --> <!-- Basic Information -->
<div style="display: grid; grid-template-columns: 1fr; gap: 1rem;"> <div style="display: flex; flex-direction: column; gap: 1.5rem;">
<!-- Tool Name --> <!-- Tool Name -->
<div> <div>
<label for="tool-name" style="display: block; margin-bottom: 0.5rem; font-weight: 600;"> <label for="tool-name" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
@ -129,37 +129,39 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
</div> </div>
<!-- Categories --> <!-- Categories -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.5rem;">
<!-- Domains --> <!-- Domains -->
<div> <div>
<label for="tool-domains" style="display: block; margin-bottom: 0.5rem; font-weight: 600;"> <label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Forensische Domänen Forensische Domänen
</label> </label>
<select id="tool-domains" name="domains" multiple size="4"> <div id="tool-domains" style="display: grid; gap: 0.25rem; font-size: 0.875rem;">
{domains.map(domain => ( {domains.map(domain => (
<option value={domain.id} <label class="checkbox-wrapper">
selected={editTool?.domains?.includes(domain.id)}> <input type="checkbox" name="domains" value={domain.id}
{domain.name} checked={editTool?.domains?.includes(domain.id)} />
</option> <span>{domain.name}</span>
</label>
))} ))}
</select> </div>
<div class="field-help">Strg gedrückt halten, um mehrere zu selektieren. Freilassen, wenn es zu keiner Domäne passt.</div> <div class="field-help">Mehrfachauswahl erlaubt</div>
</div> </div>
<!-- Phases --> <!-- Phases -->
<div> <div>
<label for="tool-phases" style="display: block; margin-bottom: 0.5rem; font-weight: 600;"> <label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Ermittlungsphasen Ermittlungsphasen
</label> </label>
<select id="tool-phases" name="phases" multiple size="4"> <div id="tool-phases" style="display: grid; gap: 0.25rem; font-size: 0.875rem;">
{phases.map(phase => ( {phases.map(phase => (
<option value={phase.id} <label class="checkbox-wrapper">
selected={editTool?.phases?.includes(phase.id)}> <input type="checkbox" name="phases" value={phase.id}
{phase.name} checked={editTool?.phases?.includes(phase.id)} />
</option> <span>{phase.name}</span>
</label>
))} ))}
</select> </div>
<div class="field-help">Zutreffende auswählen</div> <div class="field-help">Mehrfachauswahl erlaubt</div>
</div> </div>
</div> </div>
@ -287,23 +289,25 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
</div> </div>
</div> </div>
<!-- Related Concepts (for software/methods) --> <!-- Related Concepts -->
<div id="related-concepts-field" style="display: none;"> <div id="related-concepts-field" style="display: none;">
<label for="related-concepts" style="display: block; margin-bottom: 0.5rem; font-weight: 600;"> <label style="display: block; margin-bottom: 0.5rem; font-weight: 600;">
Related Concepts Related Concepts
</label> </label>
<select id="related-concepts" name="relatedConcepts" multiple size="3"> <div id="related-concepts" style="display: grid; gap: 0.25rem; font-size: 0.875rem;">
{existingTools.filter(tool => tool.type === 'concept').map(concept => ( {existingTools.filter(tool => tool.type === 'concept').map(concept => (
<option value={concept.name} <label class="checkbox-wrapper">
selected={editTool?.related_concepts?.includes(concept.name)}> <input type="checkbox" name="relatedConcepts" value={concept.name}
{concept.name} checked={editTool?.related_concepts?.includes(concept.name)} />
</option> <span>{concept.name}</span>
</label>
))} ))}
</select> </div>
<div class="field-help"> <div class="field-help">
Select concepts that users should understand when using this tool/method Wählt Konzepte, die hilfreich sind für das Verständnis dieses Tools oder dieser Methode
</div> </div>
</div> </div>
</div>
<!-- Contribution Reason --> <!-- Contribution Reason -->
<div> <div>
@ -323,9 +327,6 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
<div> <div>
<div style="display: flex; justify-content: between; align-items: center; margin-bottom: 0.5rem;"> <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 0.5rem;">
<label style="font-weight: 600;">YAML Preview</label> <label style="font-weight: 600;">YAML Preview</label>
<button type="button" id="refresh-preview" class="btn btn-secondary" style="padding: 0.25rem 0.75rem; font-size: 0.8125rem;">
Refresh Preview
</button>
</div> </div>
<pre id="yaml-preview" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 1rem; font-size: 0.8125rem; overflow-x: auto; max-height: 300px;"> <pre id="yaml-preview" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 0.375rem; padding: 1rem; font-size: 0.8125rem; overflow-x: auto; max-height: 300px;">
# YAML preview will appear here # YAML preview will appear here
@ -364,7 +365,6 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>
@ -418,6 +418,7 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Beitrag erstellen';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('contribution-form'); const form = document.getElementById('contribution-form');
if (!form) console.error('[INIT ERROR] Form not found');
const typeSelect = document.getElementById('tool-type'); const typeSelect = document.getElementById('tool-type');
const submitBtn = document.getElementById('submit-btn'); const submitBtn = document.getElementById('submit-btn');
const submitText = document.getElementById('submit-text'); const submitText = document.getElementById('submit-text');
@ -619,10 +620,6 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]`
typeSelect.addEventListener('change', updateFieldVisibility); typeSelect.addEventListener('change', updateFieldVisibility);
} }
if (refreshPreviewBtn) {
refreshPreviewBtn.addEventListener('click', updateYAMLPreview);
}
// Update preview on form changes // Update preview on form changes
if (form) { if (form) {
form.addEventListener('input', debounce(updateYAMLPreview, 500)); form.addEventListener('input', debounce(updateYAMLPreview, 500));
@ -631,8 +628,9 @@ related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]`
// Form submission // Form submission
if (form) { if (form) {
form.addEventListener('submit', async (e) => { form?.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
console.log('[DEBUG] Submit button clicked');
const errors = validateForm(); const errors = validateForm();
if (errors.length > 0) { if (errors.length > 0) {

View File

@ -279,13 +279,21 @@ input, select, textarea {
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
font-size: 0.875rem; font-size: 0.875rem;
transition: var(--transition-fast); transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
} }
input:focus, select:focus, textarea:focus { input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--color-primary); border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%); box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.1);
}
/* Form validation states */
input:invalid:not(:focus), textarea:invalid:not(:focus), select:invalid:not(:focus) {
border-color: var(--color-error);
}
input:valid:not(:focus), textarea:valid:not(:focus), select:valid:not(:focus) {
border-color: var(--color-accent);
} }
select { select {
@ -301,14 +309,67 @@ select {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
cursor: pointer;
transition: var(--transition-fast);
user-select: none;
}
.checkbox-wrapper:hover {
background-color: var(--color-bg-secondary);
border-radius: 0.25rem;
}
.checkbox-wrapper input[type="checkbox"] {
margin-right: 0.5rem;
cursor: pointer;
}
/* Scrollable checkbox containers */
.checkbox-container {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
padding: 0.75rem;
background-color: var(--color-bg);
}
.checkbox-container::-webkit-scrollbar {
width: 8px;
}
.checkbox-container::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
border-radius: 4px;
}
.checkbox-container::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
.checkbox-container::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
} }
input[type="checkbox"] { input[type="checkbox"] {
width: auto; width: 16px;
height: 16px;
accent-color: var(--color-primary);
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
} }
/* Better focus states for accessibility */
input[type="checkbox"]:focus,
input[type="text"]:focus,
input[type="url"]:focus,
textarea:focus,
select:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Consolidated Card System */ /* Consolidated Card System */
.card { .card {
background-color: var(--color-bg); background-color: var(--color-bg);
@ -689,6 +750,7 @@ input[type="checkbox"] {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgb(0 0 0 / 50%); background-color: rgb(0 0 0 / 50%);
backdrop-filter: blur(2px);
z-index: 999; z-index: 999;
} }
@ -847,6 +909,27 @@ input[type="checkbox"] {
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Loading state improvements */
.btn.loading {
opacity: 0.7;
pointer-events: none;
position: relative;
}
.btn.loading::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Collaboration Tools */ /* Collaboration Tools */
.collaboration-tools-compact { .collaboration-tools-compact {
display: flex; display: flex;
@ -1440,6 +1523,11 @@ This will literally assault the user's retinas. They'll need sunglasses to look
} }
} }
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Consolidated Responsive Design */ /* Consolidated Responsive Design */
@media (width <= 1200px) { @media (width <= 1200px) {
.modals-side-by-side #tool-details-primary.active, .modals-side-by-side #tool-details-primary.active,
@ -1520,6 +1608,10 @@ This will literally assault the user's retinas. They'll need sunglasses to look
width: 95%; width: 95%;
max-width: none; max-width: none;
} }
.form-grid.two-columns {
grid-template-columns: 1fr;
}
} }
@media (width <= 640px) { @media (width <= 640px) {
@ -1572,6 +1664,15 @@ This will literally assault the user's retinas. They'll need sunglasses to look
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 0.5rem; gap: 0.5rem;
} }
.card {
padding: 1rem;
}
.form-grid {
gap: 0.75rem;
}
.checkbox-container {
max-height: 150px;
}
} }
@media (width <= 480px) { @media (width <= 480px) {
@ -1844,4 +1945,98 @@ This will literally assault the user's retinas. They'll need sunglasses to look
.flex-start { .flex-start {
display: flex; display: flex;
align-items: center; align-items: center;
}
.field-help {
font-size: 0.8125rem;
color: var(--color-text-secondary);
line-height: 1.4;
}
/* Improved field error styling */
.field-error {
color: var(--color-error);
font-size: 0.8125rem;
margin-top: 0.25rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.field-error::before {
content: "⚠";
font-size: 0.75rem;
}
/* Form section improvements */
.form-section {
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
background-color: var(--color-bg);
}
.form-section h3 {
margin-top: 0;
margin-bottom: 1rem;
color: var(--color-primary);
font-size: 1.125rem;
}
/* Success/warning notices in forms */
.form-notice {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
border-left: 3px solid;
}
.form-notice.success {
background-color: var(--color-oss-bg);
border-left-color: var(--color-accent);
color: var(--color-text);
}
.form-notice.warning {
background-color: var(--color-hosted-bg);
border-left-color: var(--color-warning);
color: var(--color-text);
}
.form-notice.info {
background-color: var(--color-bg-secondary);
border-left-color: var(--color-primary);
color: var(--color-text);
}
/* Better form grid layout */
.form-grid {
display: grid;
gap: 1rem;
}
.form-grid.two-columns {
grid-template-columns: 1fr 1fr;
}
/* Better spacing for form elements */
.form-group {
margin-bottom: 1.5rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--color-text);
}
.form-label.required::after {
content: " *";
color: var(--color-error);
} }

View File

@ -1,4 +1,4 @@
// src/utils/gitContributions.ts - Enhanced for Phase 3 // src/utils/gitContributions.ts - Enhanced for Phase 3 with YAML preservation
import { execSync, spawn } from 'child_process'; import { execSync, spawn } from 'child_process';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { load, dump } from 'js-yaml'; import { load, dump } from 'js-yaml';
@ -285,144 +285,291 @@ export class GitContributionManager {
} }
} }
/**
private generateToolYAML(tool: any): string { * CRITICAL FIX: Preserve YAML formatting while updating tools
// Clean up the tool object - remove null/undefined values * This prevents the complete rewrite that destroys multiline descriptions
const cleanTool: any = { */
name: tool.name, private async preserveYamlFormat(toolsPath: string, newTool: any, isEdit: boolean): Promise<string> {
type: tool.type, const originalContent = await this.readFile(toolsPath);
description: tool.description, const yamlData: any = load(originalContent);
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 {
await this.createBranch(branchName);
const toolsPath = 'src/data/tools.yaml';
const originalYaml = await this.readFile(toolsPath);
const yamlData: any = load(originalYaml);
if (!yamlData.tools || !Array.isArray(yamlData.tools)) { if (!yamlData.tools || !Array.isArray(yamlData.tools)) {
throw new Error('Invalid tools.yaml format'); throw new Error('Invalid tools.yaml format');
} }
if (data.type === 'edit') { if (isEdit) {
yamlData.tools = yamlData.tools.filter((t: any) => (t.name || '').toLowerCase() !== data.tool.name.toLowerCase()); // Find and replace existing tool
const toolIndex = yamlData.tools.findIndex((t: any) =>
(t.name || '').toLowerCase() === newTool.name.toLowerCase()
);
if (toolIndex >= 0) {
yamlData.tools[toolIndex] = newTool;
} else {
throw new Error('Tool to edit not found');
}
} else {
// Add new tool - insert alphabetically
const insertIndex = yamlData.tools.findIndex((t: any) =>
(t.name || '').toLowerCase() > newTool.name.toLowerCase()
);
if (insertIndex >= 0) {
yamlData.tools.splice(insertIndex, 0, newTool);
} else {
yamlData.tools.push(newTool);
}
} }
yamlData.tools.push(data.tool); // Split original content into sections to preserve formatting
const lines = originalContent.split('\n');
const newYaml = dump(yamlData, { lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false, indent: 2 }); const toolsStartIndex = lines.findIndex(line => line.trim() === 'tools:');
await this.writeFile(toolsPath, newYaml);
const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`;
await this.commitChanges(commitMessage);
await this.pushBranch(branchName);
const prUrl = await this.createPullRequest(
branchName,
`${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`,
`Automated contribution for ${data.tool.name}`
);
return {
success: true,
message: `Tool contribution submitted successfully`,
prUrl,
branchName
};
} catch (error) { if (toolsStartIndex === -1) {
// Cleanup on failure // Fallback to full rewrite if structure is unexpected
try { console.warn('Could not find tools section, falling back to full YAML rewrite');
await this.deleteBranch(branchName); return dump(yamlData, {
} catch (cleanupError) { lineWidth: -1,
console.error('Failed to cleanup branch:', cleanupError); noRefs: true,
quotingType: '"',
forceQuotes: false,
indent: 2
});
} }
// Preserve header (everything before tools:)
const header = lines.slice(0, toolsStartIndex + 1).join('\n');
throw error; // Find footer (domains, phases, etc.)
const domainsStartIndex = lines.findIndex(line => line.trim() === 'domains:');
const footer = domainsStartIndex >= 0 ? '\n' + lines.slice(domainsStartIndex).join('\n') : '';
// Generate only the tools section with proper formatting
const toolsYaml = yamlData.tools.map((tool: any) => {
return this.formatToolYaml(tool);
}).join('');
return header + '\n' + toolsYaml + footer;
} }
}
/**
* Format a single tool entry preserving multiline descriptions
*/
private formatToolYaml(tool: any): string {
let toolEntry = ` - name: "${tool.name}"\n`;
if (tool.icon) toolEntry += ` icon: "${tool.icon}"\n`;
toolEntry += ` type: ${tool.type}\n`;
// PRESERVE multiline description format for longer descriptions
if (tool.description && tool.description.length > 80) {
toolEntry += ` description: >-\n`;
const words: string[] = tool.description.split(' ');
const lines: string[] = [];
let currentLine: string = '';
words.forEach((word: string) => {
if ((currentLine + ' ' + word).length > 80) {
if (currentLine) lines.push(currentLine);
currentLine = word;
} else {
currentLine = currentLine ? currentLine + ' ' + word : word;
}
});
if (currentLine) lines.push(currentLine);
lines.forEach((line: string) => {
toolEntry += ` ${line}\n`;
});
} else {
toolEntry += ` description: "${tool.description}"\n`;
}
// Add array fields
if (tool.domains && tool.domains.length > 0) {
toolEntry += ` domains:\n`;
tool.domains.forEach((domain: string) => {
toolEntry += ` - ${domain}\n`;
});
}
if (tool.phases && tool.phases.length > 0) {
toolEntry += ` phases:\n`;
tool.phases.forEach((phase: string) => {
toolEntry += ` - ${phase}\n`;
});
}
if (tool.platforms && tool.platforms.length > 0) {
toolEntry += ` platforms:\n`;
tool.platforms.forEach((platform: string) => {
toolEntry += ` - ${platform}\n`;
});
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
toolEntry += ` related_concepts:\n`;
tool.related_concepts.forEach((concept: string) => {
toolEntry += ` - ${concept}\n`;
});
}
if (tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length > 0) {
toolEntry += ` domain-agnostic-software:\n`;
tool['domain-agnostic-software'].forEach((item: string) => {
toolEntry += ` - ${item}\n`;
});
}
// Add scalar fields
toolEntry += ` skillLevel: ${tool.skillLevel}\n`;
if (tool.accessType) toolEntry += ` accessType: ${tool.accessType}\n`;
toolEntry += ` url: ${tool.url}\n`;
if (tool.projectUrl) toolEntry += ` projectUrl: ${tool.projectUrl}\n`;
if (tool.license) toolEntry += ` license: ${tool.license}\n`;
if (tool.knowledgebase) toolEntry += ` knowledgebase: ${tool.knowledgebase}\n`;
if (tool.tags && tool.tags.length > 0) {
toolEntry += ` tags:\n`;
tool.tags.forEach((tag: string) => {
toolEntry += ` - ${tag}\n`;
});
}
return toolEntry;
}
private generateToolYAML(tool: any): string {
// 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 {
await this.createBranch(branchName);
const toolsPath = 'src/data/tools.yaml';
// CRITICAL FIX: Use format-preserving method instead of dump()
const newYaml = await this.preserveYamlFormat(toolsPath, data.tool, data.type === 'edit');
await this.writeFile(toolsPath, newYaml);
const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
Submitted by: ${data.metadata.submitter}
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
await this.commitChanges(commitMessage);
await this.pushBranch(branchName);
// Generate tool YAML for PR description
const toolYaml = this.generateToolYAML(data.tool);
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 { 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'} **Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
### Tool Details ### Tool Details
- **Name:** ${data.tool.name} - **Name:** ${data.tool.name}
- **Description:** ${data.tool.description} - **Description:** ${data.tool.description}
- **URL:** ${data.tool.url} - **URL:** ${data.tool.url}
- **Skill Level:** ${data.tool.skillLevel} - **Skill Level:** ${data.tool.skillLevel}
${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''} ${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''}
${data.tool.license ? `- **License:** ${data.tool.license}` : ''} ${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''} ${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''}
${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''} ${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''}
- **Domains:** ${data.tool.domains.join(', ')} - **Domains:** ${data.tool.domains.join(', ')}
- **Phases:** ${data.tool.phases.join(', ')} - **Phases:** ${data.tool.phases.join(', ')}
${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.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.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.metadata.reason ? `### Reason for Contribution ${data.metadata.reason ? `### Reason for Contribution
${data.metadata.reason} ${data.metadata.reason}
` : ''}### Raw Tool Data (Copy & Paste Ready) ` : ''}### Raw Tool Data (Copy & Paste Ready)
\`\`\`yaml \`\`\`yaml
${toolYaml}\`\`\` ${toolYaml}\`\`\`
### For Maintainers ### For Maintainers
**To add this tool to tools.yaml:** **To add this tool to tools.yaml:**
1. Copy the YAML data above 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'} 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 3. Verify all fields are correct
4. Test that the tool displays properly 4. Test that the tool displays properly
5. Close this PR 5. Close this PR
### Review Checklist ### Review Checklist
- [ ] Tool information is accurate and complete - [ ] Tool information is accurate and complete
- [ ] Description is clear and informative - [ ] Description is clear and informative
- [ ] Domains and phases are correctly assigned - [ ] Domains and phases are correctly assigned
- [ ] Tags are relevant and consistent with existing tools - [ ] Tags are relevant and consistent with existing tools
- [ ] License information is correct (for software) - [ ] License information is correct (for software)
- [ ] URLs are valid and accessible - [ ] URLs are valid and accessible
- [ ] No duplicate tool entries - [ ] No duplicate tool entries
- [ ] YAML syntax is valid - [ ] YAML syntax is valid
--- ---
*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`; *This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
} }
} }