- Fixed Bootstrap bg-dark class to use better contrasting color - Added comprehensive text-muted contrast fixes for various contexts - Improved dark theme colors for better accessibility - Fixed CSS inheritance issues for code elements in dark contexts - All color choices meet WCAG AA contrast requirements
671 lines
21 KiB
JavaScript
671 lines
21 KiB
JavaScript
// UnitForge Templates JavaScript
|
|
// Handles the templates browser functionality
|
|
|
|
class TemplatesBrowser {
|
|
constructor() {
|
|
this.templates = [];
|
|
this.filteredTemplates = [];
|
|
this.currentCategory = "all";
|
|
this.currentTemplate = null;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.setupEventListeners();
|
|
this.loadTemplates();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
// Search input
|
|
const searchInput = document.getElementById("searchInput");
|
|
if (searchInput) {
|
|
searchInput.addEventListener(
|
|
"keyup",
|
|
this.debounce(this.filterTemplates.bind(this), 300),
|
|
);
|
|
}
|
|
|
|
// Template form changes
|
|
document.addEventListener("change", (e) => {
|
|
if (
|
|
e.target.matches(
|
|
"#templateForm input, #templateForm select, #templateForm textarea",
|
|
)
|
|
) {
|
|
this.updatePreview();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("input", (e) => {
|
|
if (e.target.matches("#templateForm input, #templateForm textarea")) {
|
|
this.updatePreview();
|
|
}
|
|
});
|
|
}
|
|
|
|
async loadTemplates() {
|
|
const loadingState = document.getElementById("loadingState");
|
|
const templatesGrid = document.getElementById("templatesGrid");
|
|
|
|
try {
|
|
// Temporarily use static JSON file for testing
|
|
const response = await fetch("/static/templates.json");
|
|
this.templates = await response.json();
|
|
this.filteredTemplates = [...this.templates];
|
|
|
|
if (loadingState) loadingState.classList.add("d-none");
|
|
if (templatesGrid) templatesGrid.classList.remove("d-none");
|
|
|
|
this.renderTemplates();
|
|
this.updateCategoryCounts();
|
|
} catch (error) {
|
|
if (loadingState) {
|
|
loadingState.innerHTML = `
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
|
<h4>Failed to load templates</h4>
|
|
<p class="text-muted">${error.message}</p>
|
|
<button class="btn btn-primary" onclick="location.reload()">Retry</button>
|
|
</div>
|
|
`;
|
|
}
|
|
unitforge.showToast("Failed to load templates", "error");
|
|
}
|
|
}
|
|
|
|
renderTemplates() {
|
|
const grid = document.getElementById("templatesGrid");
|
|
const noResults = document.getElementById("noResults");
|
|
|
|
if (!grid) return;
|
|
|
|
if (this.filteredTemplates.length === 0) {
|
|
grid.classList.add("d-none");
|
|
if (noResults) noResults.classList.remove("d-none");
|
|
return;
|
|
}
|
|
|
|
if (noResults) noResults.classList.add("d-none");
|
|
grid.classList.remove("d-none");
|
|
|
|
grid.innerHTML = this.filteredTemplates
|
|
.map((template) => this.createTemplateCard(template))
|
|
.join("");
|
|
}
|
|
|
|
createTemplateCard(template) {
|
|
const tags = template.tags
|
|
.map((tag) => `<span class="template-tag">${this.escapeHtml(tag)}</span>`)
|
|
.join("");
|
|
|
|
const requiredParams = template.parameters.filter((p) => p.required).length;
|
|
const totalParams = template.parameters.length;
|
|
|
|
return `
|
|
<div class="col-md-6 col-lg-4">
|
|
<div class="card template-card border-0 shadow-sm" onclick="openTemplate('${template.name}')">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="card-title mb-1">${this.escapeHtml(template.name)}</h6>
|
|
<small class="opacity-75">
|
|
<i class="fas fa-${this.getUnitTypeIcon(template.unit_type)} me-1"></i>
|
|
${template.unit_type}
|
|
</small>
|
|
</div>
|
|
<span class="badge bg-light text-dark">${template.category}</span>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<p class="card-text text-muted mb-3">${this.escapeHtml(template.description)}</p>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<small class="text-muted">
|
|
<i class="fas fa-sliders-h me-1"></i>
|
|
${requiredParams}/${totalParams} parameters
|
|
</small>
|
|
</div>
|
|
|
|
${tags ? `<div class="template-tags">${tags}</div>` : ""}
|
|
</div>
|
|
<div class="card-footer bg-transparent border-0">
|
|
<div class="d-grid">
|
|
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); openTemplate('${template.name}')">
|
|
<i class="fas fa-play me-2"></i>Use Template
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getUnitTypeIcon(unitType) {
|
|
const icons = {
|
|
service: "play-circle",
|
|
timer: "clock",
|
|
socket: "plug",
|
|
mount: "hdd",
|
|
target: "bullseye",
|
|
path: "folder",
|
|
};
|
|
return icons[unitType] || "file";
|
|
}
|
|
|
|
updateCategoryCounts() {
|
|
const categories = {
|
|
all: this.templates.length,
|
|
"Web Services": 0,
|
|
"Database Services": 0,
|
|
"System Maintenance": 0,
|
|
"Container Services": 0,
|
|
"Network Services": 0,
|
|
};
|
|
|
|
this.templates.forEach((template) => {
|
|
if (categories.hasOwnProperty(template.category)) {
|
|
categories[template.category]++;
|
|
}
|
|
});
|
|
|
|
// Update count badges
|
|
const categoryMappings = {
|
|
all: "count-all",
|
|
"Web Services": "count-web",
|
|
"Database Services": "count-database",
|
|
"System Maintenance": "count-maintenance",
|
|
"Container Services": "count-container",
|
|
"Network Services": "count-network",
|
|
};
|
|
|
|
Object.keys(categories).forEach((category) => {
|
|
const elementId = categoryMappings[category];
|
|
if (elementId) {
|
|
const countElement = document.getElementById(elementId);
|
|
if (countElement) {
|
|
countElement.textContent = categories[category];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
filterTemplates() {
|
|
const searchTerm = document
|
|
.getElementById("searchInput")
|
|
.value.toLowerCase();
|
|
|
|
this.filteredTemplates = this.templates.filter((template) => {
|
|
const matchesSearch =
|
|
!searchTerm ||
|
|
template.name.toLowerCase().includes(searchTerm) ||
|
|
template.description.toLowerCase().includes(searchTerm) ||
|
|
template.tags.some((tag) => tag.toLowerCase().includes(searchTerm));
|
|
|
|
const matchesCategory =
|
|
this.currentCategory === "all" ||
|
|
template.category === this.currentCategory;
|
|
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
this.renderTemplates();
|
|
}
|
|
|
|
filterByCategory(category) {
|
|
this.currentCategory = category;
|
|
|
|
// Update active tab
|
|
document.querySelectorAll("#categoryTabs .nav-link").forEach((tab) => {
|
|
tab.classList.remove("active");
|
|
});
|
|
|
|
const tabMappings = {
|
|
all: "tab-all",
|
|
"Web Services": "tab-web",
|
|
"Database Services": "tab-database",
|
|
"System Maintenance": "tab-maintenance",
|
|
"Container Services": "tab-container",
|
|
"Network Services": "tab-network",
|
|
};
|
|
|
|
const activeTab = document.getElementById(tabMappings[category]);
|
|
if (activeTab) {
|
|
activeTab.classList.add("active");
|
|
}
|
|
|
|
this.filterTemplates();
|
|
}
|
|
|
|
clearSearch() {
|
|
const searchInput = document.getElementById("searchInput");
|
|
if (searchInput) {
|
|
searchInput.value = "";
|
|
}
|
|
this.currentCategory = "all";
|
|
|
|
// Reset active tab
|
|
document.querySelectorAll("#categoryTabs .nav-link").forEach((tab) => {
|
|
tab.classList.remove("active");
|
|
});
|
|
document.getElementById("tab-all").classList.add("active");
|
|
|
|
this.filterTemplates();
|
|
}
|
|
|
|
async openTemplate(templateName) {
|
|
try {
|
|
this.currentTemplate = await unitforge.getTemplate(templateName);
|
|
this.showTemplateModal();
|
|
} catch (error) {
|
|
unitforge.showToast(`Failed to load template: ${error.message}`, "error");
|
|
}
|
|
}
|
|
|
|
showTemplateModal() {
|
|
if (!this.currentTemplate) return;
|
|
|
|
const modal = document.getElementById("templateModal");
|
|
const title = document.getElementById("templateModalTitle");
|
|
const info = document.getElementById("templateInfo");
|
|
const parametersDiv = document.getElementById("templateParameters");
|
|
const previewFilename = document.getElementById("previewFilename");
|
|
|
|
// Update modal title
|
|
if (title) {
|
|
title.innerHTML = `
|
|
<i class="fas fa-file-code me-2"></i>
|
|
${this.escapeHtml(this.currentTemplate.name)}
|
|
`;
|
|
}
|
|
|
|
// Update template info
|
|
if (info) {
|
|
info.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-sm-3"><strong>Name:</strong></div>
|
|
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.name)}</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm-3"><strong>Type:</strong></div>
|
|
<div class="col-sm-9">
|
|
<i class="fas fa-${this.getUnitTypeIcon(this.currentTemplate.unit_type)} me-1"></i>
|
|
${this.currentTemplate.unit_type}
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm-3"><strong>Category:</strong></div>
|
|
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.category)}</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-sm-3"><strong>Description:</strong></div>
|
|
<div class="col-sm-9">${this.escapeHtml(this.currentTemplate.description)}</div>
|
|
</div>
|
|
${
|
|
this.currentTemplate.tags.length > 0
|
|
? `
|
|
<div class="row">
|
|
<div class="col-sm-3"><strong>Tags:</strong></div>
|
|
<div class="col-sm-9">
|
|
${this.currentTemplate.tags
|
|
.map(
|
|
(tag) =>
|
|
`<span class="badge bg-secondary me-1">${this.escapeHtml(tag)}</span>`,
|
|
)
|
|
.join("")}
|
|
</div>
|
|
</div>
|
|
`
|
|
: ""
|
|
}
|
|
`;
|
|
}
|
|
|
|
// Update filename
|
|
if (previewFilename) {
|
|
const defaultName = this.getDefaultName();
|
|
previewFilename.textContent = `${defaultName}.${this.currentTemplate.unit_type}`;
|
|
}
|
|
|
|
// Generate parameters form
|
|
if (parametersDiv) {
|
|
parametersDiv.innerHTML = this.generateParametersForm();
|
|
}
|
|
|
|
// Show modal
|
|
const bsModal = new bootstrap.Modal(modal);
|
|
bsModal.show();
|
|
|
|
// Update preview
|
|
this.updatePreview();
|
|
}
|
|
|
|
generateParametersForm() {
|
|
if (!this.currentTemplate || !this.currentTemplate.parameters) {
|
|
return '<p class="text-muted">No parameters required for this template.</p>';
|
|
}
|
|
|
|
return this.currentTemplate.parameters
|
|
.map((param) => {
|
|
const isRequired = param.required;
|
|
const fieldId = `param-${param.name}`;
|
|
|
|
let inputHtml = "";
|
|
|
|
switch (param.type) {
|
|
case "boolean":
|
|
inputHtml = `
|
|
<select class="form-select" id="${fieldId}" ${isRequired ? "required" : ""}>
|
|
<option value="true" ${param.default === true ? "selected" : ""}>True</option>
|
|
<option value="false" ${param.default === false ? "selected" : ""}>False</option>
|
|
</select>
|
|
`;
|
|
break;
|
|
|
|
case "choice":
|
|
const options = param.choices
|
|
.map(
|
|
(choice) =>
|
|
`<option value="${choice}" ${param.default === choice ? "selected" : ""}>${choice}</option>`,
|
|
)
|
|
.join("");
|
|
inputHtml = `
|
|
<select class="form-select" id="${fieldId}" ${isRequired ? "required" : ""}>
|
|
${!isRequired ? '<option value="">-- Select --</option>' : ""}
|
|
${options}
|
|
</select>
|
|
`;
|
|
break;
|
|
|
|
case "integer":
|
|
inputHtml = `
|
|
<input type="number" class="form-control" id="${fieldId}"
|
|
value="${param.default || ""}"
|
|
placeholder="${param.example || ""}"
|
|
${isRequired ? "required" : ""}>
|
|
`;
|
|
break;
|
|
|
|
default: // string, list
|
|
inputHtml = `
|
|
<input type="text" class="form-control" id="${fieldId}"
|
|
value="${param.default || ""}"
|
|
placeholder="${param.example || ""}"
|
|
${isRequired ? "required" : ""}>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="parameter-group">
|
|
<label for="${fieldId}" class="parameter-label">
|
|
${this.escapeHtml(param.name)}
|
|
${isRequired ? '<span class="parameter-required">*</span>' : '<span class="parameter-optional">(optional)</span>'}
|
|
</label>
|
|
<div class="parameter-description">${this.escapeHtml(param.description)}</div>
|
|
${inputHtml}
|
|
${param.example ? `<div class="form-text">Example: ${this.escapeHtml(param.example)}</div>` : ""}
|
|
</div>
|
|
`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
getDefaultName() {
|
|
const nameParam = this.currentTemplate.parameters.find(
|
|
(p) => p.name === "name",
|
|
);
|
|
if (nameParam && nameParam.example) {
|
|
return nameParam.example;
|
|
}
|
|
return this.currentTemplate.name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
}
|
|
|
|
getFormParameters() {
|
|
const parameters = {};
|
|
|
|
if (!this.currentTemplate || !this.currentTemplate.parameters) {
|
|
return parameters;
|
|
}
|
|
|
|
this.currentTemplate.parameters.forEach((param) => {
|
|
const element = document.getElementById(`param-${param.name}`);
|
|
if (element && element.value.trim()) {
|
|
let value = element.value.trim();
|
|
|
|
// Type conversion
|
|
switch (param.type) {
|
|
case "boolean":
|
|
value = value === "true";
|
|
break;
|
|
case "integer":
|
|
value = parseInt(value);
|
|
if (isNaN(value)) value = param.default || 0;
|
|
break;
|
|
case "list":
|
|
value = value
|
|
.split(",")
|
|
.map((v) => v.trim())
|
|
.filter((v) => v);
|
|
break;
|
|
}
|
|
|
|
parameters[param.name] = value;
|
|
}
|
|
});
|
|
|
|
return parameters;
|
|
}
|
|
|
|
async updatePreview() {
|
|
if (!this.currentTemplate) return;
|
|
|
|
const preview = document.getElementById("templatePreview");
|
|
const validation = document.getElementById("validationResults");
|
|
|
|
if (!preview) return;
|
|
|
|
const parameters = this.getFormParameters();
|
|
|
|
// Check if required parameters are missing
|
|
const missingRequired = this.currentTemplate.parameters
|
|
.filter((p) => p.required && !parameters.hasOwnProperty(p.name))
|
|
.map((p) => p.name);
|
|
|
|
if (missingRequired.length > 0) {
|
|
preview.innerHTML = `<code># Please fill in required parameters: ${missingRequired.join(", ")}</code>`;
|
|
if (validation) validation.classList.add("d-none");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await unitforge.generateFromTemplate(
|
|
this.currentTemplate.name,
|
|
parameters,
|
|
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
|
);
|
|
|
|
preview.innerHTML = `<code>${this.escapeHtml(result.content)}</code>`;
|
|
|
|
// Update filename
|
|
const filenameElement = document.getElementById("previewFilename");
|
|
if (filenameElement) {
|
|
filenameElement.textContent = result.filename;
|
|
}
|
|
|
|
// Show validation results
|
|
if (validation && result.validation) {
|
|
validation.innerHTML = unitforge.formatValidationResults(
|
|
result.validation,
|
|
);
|
|
validation.classList.remove("d-none");
|
|
}
|
|
} catch (error) {
|
|
preview.innerHTML = `<code># Error generating preview: ${error.message}</code>`;
|
|
if (validation) validation.classList.add("d-none");
|
|
}
|
|
}
|
|
|
|
async validateTemplate() {
|
|
const parameters = this.getFormParameters();
|
|
const validation = document.getElementById("validationResults");
|
|
|
|
if (!validation) return;
|
|
|
|
validation.innerHTML = `
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>
|
|
Validating template...
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const result = await unitforge.generateFromTemplate(
|
|
this.currentTemplate.name,
|
|
parameters,
|
|
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
|
);
|
|
|
|
validation.innerHTML = unitforge.formatValidationResults(
|
|
result.validation,
|
|
);
|
|
|
|
if (result.validation.valid) {
|
|
unitforge.showToast("Template validation passed!", "success");
|
|
} else {
|
|
unitforge.showToast(
|
|
`Validation found ${result.validation.errors.length} error(s)`,
|
|
"warning",
|
|
);
|
|
}
|
|
} catch (error) {
|
|
validation.innerHTML = `
|
|
<div class="alert alert-danger">
|
|
<i class="fas fa-exclamation-circle me-2"></i>
|
|
Validation failed: ${error.message}
|
|
</div>
|
|
`;
|
|
unitforge.showToast("Validation failed", "error");
|
|
}
|
|
}
|
|
|
|
async generateAndDownload() {
|
|
const generateBtn = document.getElementById("generateBtn");
|
|
const originalText = generateBtn.innerHTML;
|
|
|
|
generateBtn.innerHTML =
|
|
'<i class="fas fa-spinner fa-spin me-2"></i>Generating...';
|
|
generateBtn.disabled = true;
|
|
|
|
try {
|
|
const parameters = this.getFormParameters();
|
|
const result = await unitforge.generateFromTemplate(
|
|
this.currentTemplate.name,
|
|
parameters,
|
|
`${parameters.name || "generated"}.${this.currentTemplate.unit_type}`,
|
|
);
|
|
|
|
// Download the file
|
|
unitforge.downloadTextFile(result.content, result.filename);
|
|
unitforge.showToast(`Downloaded ${result.filename}`, "success");
|
|
|
|
// Close modal
|
|
const modal = bootstrap.Modal.getInstance(
|
|
document.getElementById("templateModal"),
|
|
);
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
} catch (error) {
|
|
unitforge.showToast(`Generation failed: ${error.message}`, "error");
|
|
} finally {
|
|
generateBtn.innerHTML = originalText;
|
|
generateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
openInEditor() {
|
|
const parameters = this.getFormParameters();
|
|
|
|
// Store template data in session storage for the editor
|
|
sessionStorage.setItem(
|
|
"templateData",
|
|
JSON.stringify({
|
|
template: this.currentTemplate.name,
|
|
parameters: parameters,
|
|
}),
|
|
);
|
|
|
|
// Open editor in new tab/window or navigate
|
|
window.open("/editor", "_blank");
|
|
}
|
|
|
|
async copyPreviewToClipboard() {
|
|
const preview = document.getElementById("templatePreview");
|
|
if (preview) {
|
|
const content = preview.textContent;
|
|
await unitforge.copyToClipboard(content);
|
|
}
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Global functions for HTML onclick handlers
|
|
function filterByCategory(category) {
|
|
templatesBrowser.filterByCategory(category);
|
|
}
|
|
|
|
function filterTemplates() {
|
|
templatesBrowser.filterTemplates();
|
|
}
|
|
|
|
function clearSearch() {
|
|
templatesBrowser.clearSearch();
|
|
}
|
|
|
|
function openTemplate(templateName) {
|
|
templatesBrowser.openTemplate(templateName);
|
|
}
|
|
|
|
function validateTemplate() {
|
|
templatesBrowser.validateTemplate();
|
|
}
|
|
|
|
function generateAndDownload() {
|
|
templatesBrowser.generateAndDownload();
|
|
}
|
|
|
|
function openInEditor() {
|
|
templatesBrowser.openInEditor();
|
|
}
|
|
|
|
function copyPreviewToClipboard() {
|
|
templatesBrowser.copyPreviewToClipboard();
|
|
}
|
|
|
|
// Initialize the templates browser when DOM is ready
|
|
let templatesBrowser;
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
templatesBrowser = new TemplatesBrowser();
|
|
|
|
// Export for use in other modules
|
|
window.TemplatesBrowser = TemplatesBrowser;
|
|
window.templatesBrowser = templatesBrowser;
|
|
});
|