finalize knowledgebase commit
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
// src/pages/api/contribute/knowledgebase.ts (UPDATED - Using consolidated API responses)
|
||||
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation
|
||||
import type { APIRoute } from 'astro';
|
||||
import { withAPIAuth } from '../../../utils/auth.js';
|
||||
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
|
||||
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';
|
||||
import { GitContributionManager } from '../../../utils/gitContributions.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// Simplified schema for document-based contributions
|
||||
// Simple schema - all fields optional except for having some content
|
||||
const KnowledgebaseContributionSchema = z.object({
|
||||
toolName: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
content: z.string().default(''),
|
||||
externalLink: z.string().url().optional().or(z.literal('')),
|
||||
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']),
|
||||
toolName: z.string().optional().nullable().transform(val => val || undefined),
|
||||
title: z.string().optional().nullable().transform(val => val || undefined),
|
||||
description: z.string().optional().nullable().transform(val => val || undefined),
|
||||
content: z.string().optional().nullable().transform(val => val || undefined),
|
||||
externalLink: z.string().url().optional().nullable().catch(undefined),
|
||||
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']).optional().nullable().catch(undefined),
|
||||
categories: z.string().transform(str => {
|
||||
try { return JSON.parse(str); } catch { return []; }
|
||||
}).pipe(z.array(z.string()).default([])),
|
||||
@@ -23,32 +23,27 @@ const KnowledgebaseContributionSchema = z.object({
|
||||
}).pipe(z.array(z.string()).default([])),
|
||||
uploadedFiles: z.string().transform(str => {
|
||||
try { return JSON.parse(str); } catch { return []; }
|
||||
}).pipe(z.array(z.any()).default([]))
|
||||
}).pipe(z.array(z.any()).default([])),
|
||||
reason: z.string().optional().nullable().transform(val => val || undefined)
|
||||
});
|
||||
|
||||
interface KnowledgebaseContributionData {
|
||||
type: 'add';
|
||||
article: {
|
||||
toolName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
externalLink?: string;
|
||||
difficulty: string;
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
uploadedFiles: any[];
|
||||
};
|
||||
metadata: {
|
||||
submitter: string;
|
||||
reason?: string;
|
||||
};
|
||||
toolName?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
content?: string;
|
||||
externalLink?: string;
|
||||
difficulty?: string;
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
uploadedFiles: any[];
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
|
||||
const RATE_LIMIT_MAX = 5; // Max 5 submissions per hour per user
|
||||
const RATE_LIMIT_MAX = 3; // Max 3 submissions per hour per user
|
||||
|
||||
function checkRateLimit(userEmail: string): boolean {
|
||||
const now = Date.now();
|
||||
@@ -67,205 +62,23 @@ function checkRateLimit(userEmail: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateKnowledgebaseData(data: any): { valid: boolean; errors?: string[] } {
|
||||
// Only check that they provided SOMETHING
|
||||
const hasContent = data.content?.trim().length > 0;
|
||||
const hasLink = data.externalLink?.trim().length > 0;
|
||||
const hasFiles = data.uploadedFiles?.length > 0;
|
||||
function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid: boolean; errors?: string[] } {
|
||||
// Very minimal validation - just check that SOMETHING was provided
|
||||
// Use nullish coalescing to avoid “possibly undefined” errors in strict mode
|
||||
const hasContent = (data.content ?? '').trim().length > 0;
|
||||
const hasLink = (data.externalLink ?? '').trim().length > 0;
|
||||
const hasFiles = Array.isArray(data.uploadedFiles) && data.uploadedFiles.length > 0;
|
||||
const hasTitle = (data.title ?? '').trim().length > 0;
|
||||
const hasDescription = (data.description ?? '').trim().length > 0;
|
||||
|
||||
if (!hasContent && !hasLink && !hasFiles) {
|
||||
return { valid: false, errors: ['Must provide content, link, or files'] };
|
||||
if (!hasContent && !hasLink && !hasFiles && !hasTitle && !hasDescription) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: ['Please provide at least a title, description, content, external link, or upload files']
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true }; // That's it - maximum freedom
|
||||
}
|
||||
|
||||
function generateArticleSlug(title: string, toolName: string): string {
|
||||
const baseSlug = title.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
const toolSlug = toolName.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
return `${toolSlug}-${baseSlug}`;
|
||||
}
|
||||
|
||||
function generateMarkdownContent(article: any): string {
|
||||
const now = new Date();
|
||||
|
||||
// Generate frontmatter
|
||||
const frontmatter = {
|
||||
title: article.title,
|
||||
tool_name: article.toolName,
|
||||
description: article.description,
|
||||
last_updated: now.toISOString().split('T')[0],
|
||||
author: 'CC24-Team',
|
||||
difficulty: article.difficulty,
|
||||
categories: article.categories,
|
||||
tags: article.tags,
|
||||
review_status: 'pending_review'
|
||||
};
|
||||
|
||||
const frontmatterYaml = Object.entries(frontmatter)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return `${key}: [${value.map(v => `"${v}"`).join(', ')}]`;
|
||||
} else {
|
||||
return `${key}: "${value}"`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// Generate content sections
|
||||
let content = `---\n${frontmatterYaml}\n---\n\n`;
|
||||
content += `# ${article.title}\n\n`;
|
||||
content += `${article.description}\n\n`;
|
||||
|
||||
// Add user content if provided
|
||||
if (article.content && article.content.trim().length > 0) {
|
||||
content += `## Content\n\n${article.content}\n\n`;
|
||||
}
|
||||
|
||||
// Add external link if provided
|
||||
if (article.externalLink && article.externalLink.trim().length > 0) {
|
||||
content += `## External Resources\n\n`;
|
||||
content += `- [External Documentation](${article.externalLink})\n\n`;
|
||||
}
|
||||
|
||||
// Add uploaded files section
|
||||
if (article.uploadedFiles && article.uploadedFiles.length > 0) {
|
||||
content += `## Uploaded Files\n\n`;
|
||||
article.uploadedFiles.forEach((file: any) => {
|
||||
const fileType = file.name.toLowerCase();
|
||||
let icon = '📎';
|
||||
if (fileType.includes('.pdf')) icon = '📄';
|
||||
else if (fileType.match(/\.(png|jpg|jpeg|gif|webp)$/)) icon = '🖼️';
|
||||
else if (fileType.match(/\.(mp4|webm|mov|avi)$/)) icon = '🎥';
|
||||
else if (fileType.match(/\.(doc|docx)$/)) icon = '📝';
|
||||
else if (fileType.match(/\.(zip|tar|gz)$/)) icon = '📦';
|
||||
|
||||
content += `- ${icon} [${file.name}](${file.url})\n`;
|
||||
});
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
content += `---\n\n`;
|
||||
content += `*This article was contributed via the CC24-Hub knowledge base submission system.*\n`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Extended GitContributionManager for knowledgebase
|
||||
class KnowledgebaseGitManager extends GitContributionManager {
|
||||
async submitKnowledgebaseContribution(data: KnowledgebaseContributionData): Promise<{success: boolean, message: string, prUrl?: string, branchName?: string}> {
|
||||
const branchName = `kb-add-${Date.now()}`;
|
||||
|
||||
try {
|
||||
// Create branch
|
||||
await this.createBranch(branchName);
|
||||
|
||||
// Generate file content
|
||||
const slug = generateArticleSlug(data.article.title, data.article.toolName);
|
||||
const markdownContent = generateMarkdownContent(data.article);
|
||||
|
||||
// Write article file
|
||||
const articlePath = `src/content/knowledgebase/${slug}.md`;
|
||||
await this.writeFile(articlePath, markdownContent);
|
||||
|
||||
// Commit changes
|
||||
const commitMessage = `Add knowledgebase contribution: ${data.article.title}
|
||||
|
||||
Contributed by: ${data.metadata.submitter}
|
||||
Tool: ${data.article.toolName}
|
||||
Type: Document-based contribution
|
||||
Difficulty: ${data.article.difficulty}
|
||||
Categories: ${data.article.categories.join(', ')}
|
||||
Files: ${data.article.uploadedFiles.length} uploaded
|
||||
${data.article.externalLink ? `External Link: ${data.article.externalLink}` : ''}
|
||||
|
||||
${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,
|
||||
`Add KB Article: ${data.article.title}`,
|
||||
this.generateKnowledgebasePRDescription(data)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Knowledge base article 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 generateKnowledgebasePRDescription(data: KnowledgebaseContributionData): string {
|
||||
return `## Knowledge Base Article: ${data.article.title}
|
||||
|
||||
**Tool:** ${data.article.toolName}
|
||||
**Submitted by:** ${data.metadata.submitter}
|
||||
**Difficulty:** ${data.article.difficulty}
|
||||
|
||||
### Article Details
|
||||
- **Description:** ${data.article.description}
|
||||
- **Categories:** ${data.article.categories.length > 0 ? data.article.categories.join(', ') : 'None'}
|
||||
- **Tags:** ${data.article.tags.length > 0 ? data.article.tags.join(', ') : 'None'}
|
||||
- **Content:** ${data.article.content && data.article.content.trim().length > 0 ? `~${data.article.content.split(/\s+/).length} words` : 'Document/link-based'}
|
||||
- **Uploaded Files:** ${data.article.uploadedFiles.length} files
|
||||
${data.article.externalLink ? `- **External Link:** ${data.article.externalLink}` : ''}
|
||||
|
||||
${data.metadata.reason ? `### Reason for Contribution
|
||||
${data.metadata.reason}
|
||||
|
||||
` : ''}### Content Overview
|
||||
${data.article.content && data.article.content.trim().length > 0 ? `
|
||||
**Provided Content:**
|
||||
${data.article.content.substring(0, 200)}${data.article.content.length > 200 ? '...' : ''}
|
||||
` : ''}
|
||||
${data.article.uploadedFiles.length > 0 ? `
|
||||
**Uploaded Files:**
|
||||
${data.article.uploadedFiles.map((file: any) => `- ${file.name} (${file.url})`).join('\n')}
|
||||
` : ''}
|
||||
|
||||
### Review Checklist
|
||||
- [ ] Article content is accurate and helpful
|
||||
- [ ] All uploaded files are accessible and appropriate
|
||||
- [ ] External links are valid and safe
|
||||
- [ ] Categories and tags are relevant
|
||||
- [ ] Difficulty level is appropriate
|
||||
- [ ] Content is well-organized and clear
|
||||
- [ ] No sensitive or inappropriate content
|
||||
- [ ] Proper attribution for external sources
|
||||
|
||||
### Files Changed
|
||||
- \`src/content/knowledgebase/${generateArticleSlug(data.article.title, data.article.toolName)}.md\` (new article)
|
||||
|
||||
---
|
||||
*This contribution was submitted via the CC24-Hub document-based knowledge base system.*`;
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
@@ -288,12 +101,12 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch (error) {
|
||||
return apiSpecial.invalidJSON();
|
||||
return apiError.badRequest('Invalid form data');
|
||||
}
|
||||
|
||||
const rawData = Object.fromEntries(formData);
|
||||
|
||||
// Validate request data
|
||||
// Validate and sanitize data
|
||||
let validatedData;
|
||||
try {
|
||||
validatedData = KnowledgebaseContributionSchema.parse(rawData);
|
||||
@@ -308,39 +121,40 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
return apiError.badRequest('Invalid request data');
|
||||
}
|
||||
|
||||
// Additional validation
|
||||
const kbValidation = validateKnowledgebaseData(validatedData);
|
||||
if (!kbValidation.valid) {
|
||||
return apiError.validation('Content validation failed', kbValidation.errors);
|
||||
// Basic content validation
|
||||
const contentValidation = validateKnowledgebaseData(validatedData);
|
||||
if (!contentValidation.valid) {
|
||||
return apiError.validation('Content validation failed', contentValidation.errors);
|
||||
}
|
||||
|
||||
// Prepare contribution data
|
||||
const contributionData: KnowledgebaseContributionData = {
|
||||
type: 'add',
|
||||
article: validatedData,
|
||||
metadata: {
|
||||
submitter: userEmail,
|
||||
reason: rawData.reason as string || undefined
|
||||
}
|
||||
};
|
||||
|
||||
// Submit contribution via Git
|
||||
const gitManager = new KnowledgebaseGitManager();
|
||||
const result = await gitManager.submitKnowledgebaseContribution(contributionData);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[KB CONTRIBUTION] "${validatedData.title}" for ${validatedData.toolName} by ${userEmail} - PR: ${result.prUrl}`);
|
||||
|
||||
return apiResponse.created({
|
||||
message: result.message,
|
||||
prUrl: result.prUrl,
|
||||
branchName: result.branchName
|
||||
// Submit as issue via Git
|
||||
try {
|
||||
const gitManager = new GitContributionManager();
|
||||
const result = await gitManager.submitKnowledgebaseContribution({
|
||||
...validatedData,
|
||||
submitter: userEmail
|
||||
});
|
||||
} else {
|
||||
console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title}" by ${userEmail}: ${result.message}`);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[KB CONTRIBUTION] "${validatedData.title || 'Article'}" by ${userEmail} - Issue: ${result.issueUrl}`);
|
||||
|
||||
return apiResponse.created({
|
||||
success: true,
|
||||
message: result.message,
|
||||
issueUrl: result.issueUrl,
|
||||
issueNumber: result.issueNumber
|
||||
});
|
||||
} else {
|
||||
console.error(`[KB CONTRIBUTION FAILED] "${validatedData.title || 'Article'}" by ${userEmail}: ${result.message}`);
|
||||
|
||||
return apiServerError.internal(`Contribution failed: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[KB GIT ERROR] "${validatedData.title || 'Article'}" by ${userEmail}:`, error);
|
||||
|
||||
return apiServerError.internal(`Contribution failed: ${result.message}`);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Git operation failed';
|
||||
return apiServerError.internal(`Submission failed: ${errorMessage}`);
|
||||
}
|
||||
|
||||
}, 'Knowledgebase contribution processing failed');
|
||||
};
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ const ContributionRequestSchema = z.object({
|
||||
// Rate limiting
|
||||
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
|
||||
const RATE_LIMIT_MAX = 5; // 5 contributions per hour per user
|
||||
const RATE_LIMIT_MAX = 15; // 15 contributions per hour per user
|
||||
|
||||
function checkRateLimit(userId: string): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
// src/pages/contribute/knowledgebase.astro
|
||||
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import { withAuth } from '../../utils/auth.js';
|
||||
import { getToolsData } from '../../utils/dataService.js';
|
||||
@@ -9,39 +9,60 @@ export const prerender = false;
|
||||
// Check authentication
|
||||
const authResult = await withAuth(Astro);
|
||||
if (authResult instanceof Response) {
|
||||
return authResult; // Redirect to login
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const { authenticated, userEmail, userId } = authResult;
|
||||
|
||||
// Load tools for reference (optional dropdown)
|
||||
const data = await getToolsData();
|
||||
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
---
|
||||
|
||||
<BaseLayout title="Contribute Knowledge Base Article" description="Submit articles and documentation for the CC24-Hub knowledge base">
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="page-header">
|
||||
<h1>Submit Knowledge Base Article</h1>
|
||||
<p>Upload documents, provide links, or submit articles for review by maintainers.</p>
|
||||
</div>
|
||||
<BaseLayout title="Contribute Knowledge Base Article">
|
||||
<div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="hero-section">
|
||||
<h1>Submit Knowledge Base Article</h1>
|
||||
<p>Share documentation, tutorials, or insights about DFIR tools and methods. Your contribution will be submitted as an issue for maintainer review.</p>
|
||||
{userEmail && <p><strong>Submitting as:</strong> {userEmail}</p>}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form id="kb-form" style="padding: 2rem;">
|
||||
<!-- Tool Selection -->
|
||||
<div class="form-group">
|
||||
<label for="tool-name" class="form-label">Related Tool</label>
|
||||
<select id="tool-name" name="toolName" class="form-input">
|
||||
<option value="">Select a tool</option>
|
||||
{sortedTools.map(tool => (
|
||||
<option value={tool.name}>{tool.name} ({tool.type})</option>
|
||||
))}
|
||||
</select>
|
||||
<!-- Main Form -->
|
||||
<div class="card">
|
||||
<form id="kb-form" novalidate>
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">Basic Information</h3>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<div class="form-group">
|
||||
<label for="tool-name" class="form-label">Related Tool (Optional)</label>
|
||||
<select id="tool-name" name="toolName" class="form-input">
|
||||
<option value="">Select a tool...</option>
|
||||
{sortedTools.map(tool => (
|
||||
<option value={tool.name}>{tool.name} ({tool.type})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="difficulty" class="form-label">Difficulty Level (Optional)</label>
|
||||
<select id="difficulty" name="difficulty" class="form-input">
|
||||
<option value="">Select difficulty...</option>
|
||||
<option value="novice">Novice</option>
|
||||
<option value="beginner">Beginner</option>
|
||||
<option value="intermediate">Intermediate</option>
|
||||
<option value="advanced">Advanced</option>
|
||||
<option value="expert">Expert</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Title -->
|
||||
<div class="form-group">
|
||||
<label for="title" class="form-label">Article Title</label>
|
||||
<label for="title" class="form-label">Article Title (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
@@ -52,36 +73,36 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-group">
|
||||
<label for="description" class="form-label">Description</label>
|
||||
<label for="description" class="form-label">Description (Optional)</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
maxlength="300"
|
||||
rows="3"
|
||||
placeholder="Brief summary of what this article covers (20-300 characters)"
|
||||
placeholder="Brief summary of what this article covers"
|
||||
class="form-input"
|
||||
></textarea>
|
||||
<small class="form-help">This will be shown in search results and article listings.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Content -->
|
||||
<!-- Content -->
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">Content</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="content" class="form-label">Article Content</label>
|
||||
<label for="content" class="form-label">Article Content (Optional)</label>
|
||||
<textarea
|
||||
id="content"
|
||||
name="content"
|
||||
rows="8"
|
||||
placeholder="Provide article content, notes, or instructions. You can also upload documents below instead."
|
||||
placeholder="Provide your content, documentation, tutorial steps, or notes here..."
|
||||
class="form-input"
|
||||
></textarea>
|
||||
<small class="form-help">Optional if you're uploading documents. Use this for additional context or instructions.</small>
|
||||
</div>
|
||||
|
||||
<!-- External Link -->
|
||||
<div class="form-group">
|
||||
<label for="external-link" class="form-label">External Link</label>
|
||||
<label for="external-link" class="form-label">External Link (Optional)</label>
|
||||
<input
|
||||
type="url"
|
||||
id="external-link"
|
||||
@@ -89,72 +110,66 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
placeholder="https://example.com/documentation"
|
||||
class="form-input"
|
||||
/>
|
||||
<small class="form-help">Link to external documentation, tutorials, or resources.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Section -->
|
||||
<!-- File Upload -->
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">Upload Files</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Upload Documents</label>
|
||||
<label class="form-label">Documents, Images, Videos (Optional)</label>
|
||||
<div class="upload-area" id="upload-area">
|
||||
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4" style="display: none;">
|
||||
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" style="display: none;">
|
||||
<div class="upload-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
<p style="margin: 0.5rem 0 0 0;">Click to select files or drag & drop</p>
|
||||
<p>Click to select files or drag & drop</p>
|
||||
<small>PDFs, documents, images, archives, etc.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="file-list" style="margin-top: 1rem; display: none;">
|
||||
<div id="file-list" class="file-list" style="display: none;">
|
||||
<h5>Selected Files</h5>
|
||||
<div id="files-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Difficulty Level -->
|
||||
<div class="form-group">
|
||||
<label for="difficulty" class="form-label">Difficulty Level</label>
|
||||
<select id="difficulty" name="difficulty" class="form-input">
|
||||
<option value="">Select difficulty</option>
|
||||
<option value="novice">Novice - No prior experience needed</option>
|
||||
<option value="beginner">Beginner - Basic familiarity helpful</option>
|
||||
<option value="intermediate">Intermediate - Some experience required</option>
|
||||
<option value="advanced">Advanced - Significant experience needed</option>
|
||||
<option value="expert">Expert - Deep technical knowledge required</option>
|
||||
</select>
|
||||
<!-- Additional Information -->
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">Additional Information</h3>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<div class="form-group">
|
||||
<label for="categories" class="form-label">Categories (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="categories"
|
||||
name="categories"
|
||||
placeholder="setup, configuration, troubleshooting"
|
||||
class="form-input"
|
||||
/>
|
||||
<small class="form-help">Comma-separated categories</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="tags" class="form-label">Tags (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tags"
|
||||
name="tags"
|
||||
placeholder="installation, docker, linux, windows"
|
||||
class="form-input"
|
||||
/>
|
||||
<small class="form-help">Comma-separated tags</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div class="form-group">
|
||||
<label for="categories" class="form-label">Categories</label>
|
||||
<input
|
||||
type="text"
|
||||
id="categories"
|
||||
name="categories"
|
||||
placeholder="setup, configuration, troubleshooting"
|
||||
class="form-input"
|
||||
/>
|
||||
<small class="form-help">Comma-separated categories that describe the article type.</small>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="form-group">
|
||||
<label for="tags" class="form-label">Tags</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tags"
|
||||
name="tags"
|
||||
placeholder="installation, docker, linux, windows"
|
||||
class="form-input"
|
||||
/>
|
||||
<small class="form-help">Comma-separated tags for better searchability.</small>
|
||||
</div>
|
||||
|
||||
<!-- Reason for Contribution -->
|
||||
<div class="form-group">
|
||||
<label for="reason" class="form-label">Reason for Contribution</label>
|
||||
<label for="reason" class="form-label">Reason for Contribution (Optional)</label>
|
||||
<textarea
|
||||
id="reason"
|
||||
name="reason"
|
||||
@@ -163,139 +178,131 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
class="form-input"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div style="display: flex; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--color-border);">
|
||||
<button type="submit" id="submit-btn" class="btn btn-accent" style="flex: 1;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
</svg>
|
||||
Submit Article
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<div class="form-actions">
|
||||
<a href="/" class="btn btn-secondary">Cancel</a>
|
||||
<button type="submit" id="submit-btn" class="btn btn-accent">
|
||||
<span id="submit-text">Submit Article</span>
|
||||
<span id="submit-spinner" style="display: none;">⏳</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Success Modal -->
|
||||
<div id="success-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
|
||||
<div class="card" style="max-width: 500px; width: 90%; margin: 2rem;">
|
||||
<div style="text-align: center;">
|
||||
<div style="background-color: var(--color-accent); color: white; width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20,6 9,17 4,12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 style="margin-bottom: 1rem;">Article Submitted!</h3>
|
||||
<p id="success-message" style="margin-bottom: 1.5rem; line-height: 1.5;"></p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<a id="pr-link" href="#" target="_blank" class="btn btn-primary" style="display: none;">View Pull Request</a>
|
||||
<a href="/" class="btn btn-secondary">Back to Home</a>
|
||||
</div>
|
||||
<div id="success-modal"
|
||||
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
|
||||
background:rgba(0,0,0,.5); z-index:1000; align-items:center; justify-content:center;">
|
||||
<div class="card" style="max-width:500px; width:90%; margin:2rem; text-align:center;">
|
||||
<div style="font-size:3rem; margin-bottom:1rem;">✅</div>
|
||||
<h3 style="margin-bottom:1rem;">Article Submitted!</h3>
|
||||
<p id="success-message" style="margin-bottom:1.5rem;">
|
||||
Your knowledge‑base article has been submitted as an issue for review by maintainers.
|
||||
</p>
|
||||
<div style="display:flex; gap:1rem; justify-content:center;">
|
||||
<a id="issue-link" href="#" target="_blank" class="btn btn-primary" style="display:none;">View Issue</a>
|
||||
<a href="/" class="btn btn-secondary">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Display -->
|
||||
<div id="message-container" style="position: fixed; top: 20px; right: 20px; z-index: 1000;"></div>
|
||||
</main>
|
||||
<!-- Message Container -->
|
||||
<div id="message-container" class="message-container"></div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
// FIXED: Properly typed interfaces for TypeScript compliance
|
||||
interface UploadedFile {
|
||||
id: string;
|
||||
file: File;
|
||||
name: string;
|
||||
uploaded: boolean;
|
||||
url?: string;
|
||||
interface UploadedFile {
|
||||
id: string;
|
||||
file: File;
|
||||
name: string;
|
||||
uploaded: boolean;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
removeFile: (fileId: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
class KnowledgebaseForm {
|
||||
private uploadedFiles: UploadedFile[] = [];
|
||||
private isSubmitting = false;
|
||||
private elements: Record<string, HTMLElement | null> = {};
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Extend Window interface for global functions
|
||||
declare global {
|
||||
interface Window {
|
||||
removeFile: (fileId: string) => void;
|
||||
private init() {
|
||||
// Get elements
|
||||
this.elements = {
|
||||
form: document.getElementById('kb-form'),
|
||||
submitBtn: document.getElementById('submit-btn'),
|
||||
submitText: document.getElementById('submit-text'),
|
||||
submitSpinner: document.getElementById('submit-spinner'),
|
||||
fileInput: document.getElementById('file-input'),
|
||||
uploadArea: document.getElementById('upload-area'),
|
||||
fileList: document.getElementById('file-list'),
|
||||
filesContainer: document.getElementById('files-container'),
|
||||
successModal: document.getElementById('success-modal')
|
||||
};
|
||||
|
||||
if (!this.elements.form || !this.elements.submitBtn) {
|
||||
console.error('[KB FORM] Critical elements missing');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
this.setupFileUpload();
|
||||
}
|
||||
|
||||
// FIXED: State management with proper typing
|
||||
let uploadedFiles: UploadedFile[] = [];
|
||||
|
||||
// FIXED: Properly typed element selection with specific HTML element types
|
||||
const elements = {
|
||||
form: document.getElementById('kb-form') as HTMLFormElement | null,
|
||||
submitBtn: document.getElementById('submit-btn') as HTMLButtonElement | null,
|
||||
fileInput: document.getElementById('file-input') as HTMLInputElement | null,
|
||||
uploadArea: document.getElementById('upload-area') as HTMLElement | null,
|
||||
fileList: document.getElementById('file-list') as HTMLElement | null,
|
||||
filesContainer: document.getElementById('files-container') as HTMLElement | null
|
||||
};
|
||||
|
||||
// Check for critical elements
|
||||
const criticalElements: Array<keyof typeof elements> = ['form', 'submitBtn'];
|
||||
const missingElements = criticalElements.filter(key => !elements[key]);
|
||||
|
||||
if (missingElements.length > 0) {
|
||||
console.error('[KB FORM ERROR] Missing critical elements:', missingElements);
|
||||
} else {
|
||||
console.log('[KB FORM DEBUG] All critical elements found, initializing form');
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
return true; // Always return true - no validation
|
||||
}
|
||||
|
||||
// Update submit button state with null checks
|
||||
function updateSubmitButton(): void {
|
||||
if (elements.submitBtn) {
|
||||
const isValid = validateForm();
|
||||
elements.submitBtn.disabled = !isValid;
|
||||
console.log('[KB FORM DEBUG] Button state:', isValid ? 'enabled' : 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// File upload handling with proper null checks
|
||||
function setupFileUpload(): void {
|
||||
if (!elements.fileInput || !elements.uploadArea) return;
|
||||
|
||||
elements.uploadArea.addEventListener('click', () => {
|
||||
if (elements.fileInput) {
|
||||
elements.fileInput.click();
|
||||
}
|
||||
});
|
||||
|
||||
elements.uploadArea.addEventListener('dragover', (e: DragEvent) => {
|
||||
private setupEventListeners() {
|
||||
// Form submission
|
||||
this.elements.form?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
if (elements.uploadArea) {
|
||||
elements.uploadArea.style.borderColor = 'var(--color-accent)';
|
||||
if (!this.isSubmitting) {
|
||||
this.handleSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
elements.uploadArea.addEventListener('dragleave', () => {
|
||||
if (elements.uploadArea) {
|
||||
elements.uploadArea.style.borderColor = 'var(--color-border)';
|
||||
}
|
||||
}
|
||||
|
||||
private setupFileUpload() {
|
||||
if (!this.elements.fileInput || !this.elements.uploadArea) return;
|
||||
|
||||
this.elements.uploadArea.addEventListener('click', () => {
|
||||
(this.elements.fileInput as HTMLInputElement)?.click();
|
||||
});
|
||||
|
||||
elements.uploadArea.addEventListener('drop', (e: DragEvent) => {
|
||||
this.elements.uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
if (elements.uploadArea) {
|
||||
elements.uploadArea.style.borderColor = 'var(--color-border)';
|
||||
}
|
||||
this.elements.uploadArea?.classList.add('drag-over');
|
||||
});
|
||||
|
||||
this.elements.uploadArea.addEventListener('dragleave', () => {
|
||||
this.elements.uploadArea?.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
this.elements.uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
this.elements.uploadArea?.classList.remove('drag-over');
|
||||
if (e.dataTransfer?.files) {
|
||||
handleFiles(Array.from(e.dataTransfer.files));
|
||||
this.handleFiles(Array.from(e.dataTransfer.files));
|
||||
}
|
||||
});
|
||||
|
||||
elements.fileInput.addEventListener('change', (e: Event) => {
|
||||
this.elements.fileInput.addEventListener('change', (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target?.files) {
|
||||
handleFiles(Array.from(target.files));
|
||||
this.handleFiles(Array.from(target.files));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleFiles(files: File[]): void {
|
||||
private handleFiles(files: File[]) {
|
||||
files.forEach(file => {
|
||||
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
const newFile: UploadedFile = {
|
||||
@@ -304,15 +311,14 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
name: file.name,
|
||||
uploaded: false
|
||||
};
|
||||
uploadedFiles.push(newFile);
|
||||
uploadFile(fileId);
|
||||
this.uploadedFiles.push(newFile);
|
||||
this.uploadFile(fileId);
|
||||
});
|
||||
renderFileList();
|
||||
updateSubmitButton();
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
async function uploadFile(fileId: string): Promise<void> {
|
||||
const fileItem = uploadedFiles.find(f => f.id === fileId);
|
||||
private async uploadFile(fileId: string) {
|
||||
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
|
||||
if (!fileItem) return;
|
||||
|
||||
const formData = new FormData();
|
||||
@@ -329,63 +335,60 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
const result = await response.json();
|
||||
fileItem.uploaded = true;
|
||||
fileItem.url = result.url;
|
||||
renderFileList();
|
||||
this.renderFileList();
|
||||
} else {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('error', `Failed to upload ${fileItem.name}`);
|
||||
removeFile(fileId);
|
||||
this.showMessage('error', `Failed to upload ${fileItem.name}`);
|
||||
this.removeFile(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(fileId: string): void {
|
||||
uploadedFiles = uploadedFiles.filter(f => f.id !== fileId);
|
||||
renderFileList();
|
||||
updateSubmitButton();
|
||||
private removeFile(fileId: string) {
|
||||
this.uploadedFiles = this.uploadedFiles.filter(f => f.id !== fileId);
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
function renderFileList(): void {
|
||||
if (!elements.filesContainer || !elements.fileList) return;
|
||||
private renderFileList() {
|
||||
if (!this.elements.filesContainer || !this.elements.fileList) return;
|
||||
|
||||
if (uploadedFiles.length > 0) {
|
||||
elements.fileList.style.display = 'block';
|
||||
elements.filesContainer.innerHTML = uploadedFiles.map(file => `
|
||||
<div class="file-item" style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: 0.25rem; margin-bottom: 0.5rem;">
|
||||
<div style="flex: 1;">
|
||||
if (this.uploadedFiles.length > 0) {
|
||||
(this.elements.fileList as HTMLElement).style.display = 'block';
|
||||
(this.elements.filesContainer as HTMLElement).innerHTML = this.uploadedFiles.map(file => `
|
||||
<div class="file-item">
|
||||
<div class="file-info">
|
||||
<strong>${file.name}</strong>
|
||||
<div style="font-size: 0.875rem; color: var(--color-text-secondary);">
|
||||
<div class="file-meta">
|
||||
${(file.file.size / 1024 / 1024).toFixed(2)} MB
|
||||
${file.uploaded ?
|
||||
'<span style="color: var(--color-success);">✓ Uploaded</span>' :
|
||||
'<span style="color: var(--color-warning);">⏳ Uploading...</span>'
|
||||
'<span class="file-status success">✓ Uploaded</span>' :
|
||||
'<span class="file-status pending">⏳ Uploading...</span>'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="window.removeFile('${file.id}')" class="btn btn-small" style="background: var(--color-danger); color: white;">Remove</button>
|
||||
<button type="button" onclick="window.removeFile('${file.id}')" class="btn btn-danger btn-small">Remove</button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
elements.fileList.style.display = 'none';
|
||||
(this.elements.fileList as HTMLElement).style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
console.log('[KB FORM DEBUG] Form submitted');
|
||||
private async handleSubmit() {
|
||||
if (this.isSubmitting) return;
|
||||
|
||||
this.isSubmitting = true;
|
||||
|
||||
if (!elements.submitBtn || !elements.form) {
|
||||
console.log('[KB FORM DEBUG] Submission blocked - form missing');
|
||||
return;
|
||||
}
|
||||
|
||||
elements.submitBtn.classList.add('loading');
|
||||
elements.submitBtn.innerHTML = '⏳ Submitting...';
|
||||
// Update UI
|
||||
(this.elements.submitBtn as HTMLButtonElement).disabled = true;
|
||||
(this.elements.submitText as HTMLElement).textContent = 'Submitting...';
|
||||
(this.elements.submitSpinner as HTMLElement).style.display = 'inline';
|
||||
|
||||
try {
|
||||
const formData = new FormData(elements.form);
|
||||
const formData = new FormData(this.elements.form as HTMLFormElement);
|
||||
|
||||
// Process categories and tags with proper null handling
|
||||
// Process categories and tags
|
||||
const categoriesValue = (formData.get('categories') as string) || '';
|
||||
const tagsValue = (formData.get('tags') as string) || '';
|
||||
|
||||
@@ -395,78 +398,83 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
|
||||
formData.set('tags', JSON.stringify(tags));
|
||||
|
||||
// Add uploaded files
|
||||
formData.set('uploadedFiles', JSON.stringify(uploadedFiles.filter(f => f.uploaded)));
|
||||
formData.set('uploadedFiles', JSON.stringify(this.uploadedFiles.filter(f => f.uploaded)));
|
||||
|
||||
console.log('[KB FORM DEBUG] Submitting to API...');
|
||||
const response = await fetch('/api/contribute/knowledgebase', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('[KB FORM DEBUG] Response status:', response.status);
|
||||
const result = await response.json();
|
||||
console.log('[KB FORM DEBUG] Response data:', result);
|
||||
|
||||
if (result.success) {
|
||||
// Show success modal
|
||||
const successModal = document.getElementById('success-modal');
|
||||
const successMessage = document.getElementById('success-message');
|
||||
const prLink = document.getElementById('pr-link') as HTMLAnchorElement;
|
||||
|
||||
if (successModal && successMessage) {
|
||||
successMessage.textContent = 'Your knowledge base article has been submitted successfully and will be reviewed by the maintainers.';
|
||||
|
||||
if (result.prUrl && prLink) {
|
||||
prLink.href = result.prUrl;
|
||||
prLink.style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
successModal.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Reset form with proper typing
|
||||
if (elements.form) {
|
||||
elements.form.reset();
|
||||
}
|
||||
uploadedFiles = [];
|
||||
renderFileList();
|
||||
updateSubmitButton();
|
||||
this.showSuccess(result);
|
||||
} else {
|
||||
showMessage('error', result.error || 'Submission failed');
|
||||
throw new Error(result.error || 'Submission failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[KB FORM ERROR] Submission error:', error);
|
||||
showMessage('error', 'An error occurred during submission');
|
||||
console.error('[KB FORM] Submission error:', error);
|
||||
this.showMessage('error', 'Submission failed. Please try again.');
|
||||
} finally {
|
||||
if (elements.submitBtn) {
|
||||
elements.submitBtn.classList.remove('loading');
|
||||
elements.submitBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/></svg> Submit Article';
|
||||
}
|
||||
this.isSubmitting = false;
|
||||
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
|
||||
(this.elements.submitText as HTMLElement).textContent = 'Submit Article';
|
||||
(this.elements.submitSpinner as HTMLElement).style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showMessage(type: 'success' | 'error' | 'warning', message: string): void {
|
||||
private showSuccess(result: any) {
|
||||
const successMessage = document.getElementById('success-message');
|
||||
const issueLink = document.getElementById('issue-link') as HTMLAnchorElement;
|
||||
|
||||
if (successMessage) {
|
||||
successMessage.textContent = 'Your knowledge base article has been submitted as an issue for review by maintainers.';
|
||||
}
|
||||
|
||||
if (result.issueUrl && issueLink) {
|
||||
issueLink.href = result.issueUrl;
|
||||
issueLink.style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
(this.elements.successModal as HTMLElement).style.display = 'flex';
|
||||
|
||||
// Reset form
|
||||
(this.elements.form as HTMLFormElement).reset();
|
||||
this.uploadedFiles = [];
|
||||
this.renderFileList();
|
||||
}
|
||||
|
||||
private showMessage(type: 'success' | 'error' | 'warning', message: string) {
|
||||
const container = document.getElementById('message-container');
|
||||
if (!container) return;
|
||||
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = `message message-${type}`;
|
||||
messageEl.style.cssText = `
|
||||
padding: 1rem; margin-bottom: 0.5rem; border-radius: 0.25rem;
|
||||
background-color: var(--color-${type === 'error' ? 'danger' : type === 'warning' ? 'warning' : 'success'});
|
||||
color: white; animation: slideIn 0.3s ease;
|
||||
`;
|
||||
messageEl.textContent = message;
|
||||
|
||||
container.appendChild(messageEl);
|
||||
setTimeout(() => messageEl.remove(), 5000);
|
||||
}
|
||||
|
||||
// Make removeFile available globally with proper typing
|
||||
window.removeFile = removeFile;
|
||||
// Public method for file removal
|
||||
public removeFileById(fileId: string) {
|
||||
this.removeFile(fileId);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
setupFileUpload();
|
||||
console.log('[KB FORM DEBUG] Form initialization complete');
|
||||
// Global instance
|
||||
let formInstance: KnowledgebaseForm;
|
||||
|
||||
// Global function for file removal
|
||||
window.removeFile = (fileId: string) => {
|
||||
if (formInstance) {
|
||||
formInstance.removeFileById(fileId);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize form
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
formInstance = new KnowledgebaseForm();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user