diff --git a/src/content/knowledgebase/concept-hash-functions b/src/content/knowledgebase/concept-hash-functions.md similarity index 100% rename from src/content/knowledgebase/concept-hash-functions rename to src/content/knowledgebase/concept-hash-functions.md diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index c1f7ee1..d8aa06b 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -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; diff --git a/src/pages/api/contribute/tool.ts b/src/pages/api/contribute/tool.ts index 7d656f1..1cb90d8 100644 --- a/src/pages/api/contribute/tool.ts +++ b/src/pages/api/contribute/tool.ts @@ -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); diff --git a/src/pages/contribute/tool.astro b/src/pages/contribute/tool.astro index 1b6babc..dec8904 100644 --- a/src/pages/contribute/tool.astro +++ b/src/pages/contribute/tool.astro @@ -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(); --- @@ -194,16 +205,27 @@ const isEdit = !!editTool; -