Updated UI

This commit is contained in:
2026-06-24 07:04:59 +02:00
committed by Jeroen Saey
parent b8905206d9
commit a416451ccb
6 changed files with 3024 additions and 263 deletions
+1318 -256
View File
File diff suppressed because it is too large Load Diff
+455 -2
View File
@@ -1610,7 +1610,7 @@
dashboardPanel.style.display = "block"; dashboardPanel.style.display = "block";
try { try {
const res = await fetch("analytics"); const res = await fetch(new URL("analytics", document.baseURI));
if (!res.ok) throw new Error("Failed to load analytics"); if (!res.ok) throw new Error("Failed to load analytics");
const analyticsData = await res.json(); const analyticsData = await res.json();
@@ -1619,7 +1619,7 @@
// Refresh every 5 seconds // Refresh every 5 seconds
if (window.dashboardInterval) clearInterval(window.dashboardInterval); if (window.dashboardInterval) clearInterval(window.dashboardInterval);
window.dashboardInterval = setInterval(async () => { window.dashboardInterval = setInterval(async () => {
const res = await fetch("analytics"); const res = await fetch(new URL("analytics", document.baseURI));
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
updateDashboard(data); updateDashboard(data);
@@ -1689,5 +1689,458 @@
`; `;
} }
/**
* Enhanced Try Pane - Advanced Request Builder
* Integrates RequestBuilder and CodeGenerator modules
*/
function renderAdvancedTryPane(group, endpoint) {
if (!endpoint) return renderTryEmpty();
const paramState = new RequestBuilder.ParameterState();
RequestBuilder.populateDefaults(endpoint, paramState);
let html = `
<div class="try-wrap">
<div class="try-header">
<div class="try-title">${escapeHtml(endpoint.method)} ${escapeHtml(endpoint.path)}</div>
${endpoint.deprecated ? '<span class="deprecated-badge">DEPRECATED</span>' : ''}
</div>
<div class="params-editor">
`;
// Path parameters
if (endpoint.parameters?.filter(p => p.in === 'path').length > 0) {
html += renderParamGroup('Path Parameters', endpoint.parameters.filter(p => p.in === 'path'), paramState);
}
// Query parameters
if (endpoint.parameters?.filter(p => p.in === 'query').length > 0) {
html += renderParamGroup('Query Parameters', endpoint.parameters.filter(p => p.in === 'query'), paramState);
}
// Headers
if (endpoint.parameters?.filter(p => p.in === 'header').length > 0) {
html += renderParamGroup('Headers', endpoint.parameters.filter(p => p.in === 'header'), paramState);
}
// Request body
if (endpoint.requestBody) {
html += `
<div class="param-group">
<div class="param-group-header">
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
Request Body
</div>
<div class="param-items">
<div class="param-input" style="height: 200px; font-family: var(--font-mono); padding: 10px;" contenteditable="true" data-request-body="true">
${endpoint.requestBody.example ? JSON.stringify(endpoint.requestBody.example, null, 2) : '{}'}
</div>
</div>
</div>
`;
}
html += `</div>`;
// Code Generator
html += `
<div class="code-generator">
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Generate Code</div>
<div class="code-lang-select" id="codeLangSelect">
`;
CodeGenerator.getSupportedLanguages().forEach(lang => {
html += `<button class="code-lang-btn" data-lang="${lang}">${lang}</button>`;
});
html += `
</div>
<div class="code-block" id="codeBlock">
// Select a language above
</div>
<button class="code-copy-btn" id="codeCopyBtn">Copy Code</button>
</div>
`;
// Response preview area
html += `
<div class="response-preview" style="display: none;" id="responsePreview">
<div class="response-header">
<span class="response-status" id="responseStatus"></span>
<span class="response-timing" id="responseTiming"></span>
</div>
<div class="response-tabs">
<button class="response-tab active" data-tab="body">Body</button>
<button class="response-tab" data-tab="headers">Headers</button>
</div>
<div class="response-body" id="responseBody"></div>
</div>
`;
html += `</div>`;
el.try.innerHTML = html;
// Bind code generator
document.querySelectorAll('.code-lang-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.code-lang-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeCodeLang = btn.dataset.lang;
updateCodePreview(endpoint, paramState);
});
});
// Initial code preview
updateCodePreview(endpoint, paramState);
// Copy code button
document.getElementById('codeCopyBtn')?.addEventListener('click', () => {
const code = document.getElementById('codeBlock').textContent;
navigator.clipboard.writeText(code).then(() => {
const btn = document.getElementById('codeCopyBtn');
btn.classList.add('copied');
btn.textContent = 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = 'Copy Code';
}, 2000);
});
});
}
/**
* Render parameter group
*/
function renderParamGroup(name, parameters, paramState) {
if (!parameters.length) return '';
const paramIn = parameters[0].in;
let html = `
<div class="param-group">
<div class="param-group-header" data-toggle-group="${paramIn}">
<svg class="param-group-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
${name}
</div>
<div class="param-items">
`;
parameters.forEach(param => {
html += `
<div class="param-row ${param.required ? 'required' : ''}">
<div class="param-name" title="${escapeHtml(param.description || param.name)}">${escapeHtml(param.name)}</div>
<input
type="text"
class="param-input"
data-param-in="${param.in}"
data-param-name="${param.name}"
placeholder="${param.schema?.description || ''}"
value="${paramState[param.in]?.[param.name] || ''}"
/>
<div class="param-type">${param.schema?.type || 'string'}</div>
<button class="param-remove" title="Remove parameter">×</button>
</div>
`;
});
html += `</div></div>`;
return html;
}
/**
* Update code preview based on current parameters
*/
function updateCodePreview(endpoint, paramState) {
const currentLangBtn = document.querySelector('.code-lang-btn.active');
const lang = currentLangBtn?.dataset.lang || 'curl';
// Gather current param values from UI
document.querySelectorAll('.param-input').forEach(input => {
const paramIn = input.dataset.paramIn;
const paramName = input.dataset.paramName;
if (paramIn && paramName) {
paramState[paramIn][paramName] = input.value;
}
});
// Get request body if present
const bodyInput = document.querySelector('[data-request-body="true"]');
if (bodyInput) {
try {
paramState.body = JSON.parse(bodyInput.textContent);
} catch (e) {
paramState.body = bodyInput.textContent;
}
}
const request = RequestBuilder.buildRequest(endpoint, paramState, window.location.origin);
const code = CodeGenerator.generate(lang, request);
document.getElementById('codeBlock').textContent = code;
}
/**
* Integration point for workspace manager
*/
function initializeWorkspaceManager() {
// Load initial workspaces
const workspaces = WorkspaceManager.getWorkspaces();
if (workspaces.length === 0) {
const defaultWs = new WorkspaceManager.Workspace('My Workspace');
const defaultCol = new WorkspaceManager.Collection('API Collection');
defaultWs.collections.push(defaultCol);
WorkspaceManager.saveWorkspace(defaultWs);
}
// Setup environment selector if environments panel exists
const envList = document.querySelector('.env-list');
if (envList) {
const environments = WorkspaceManager.getEnvironments();
if (environments.length === 0) {
const defaultEnv = new WorkspaceManager.Environment('Default Environment');
WorkspaceManager.saveEnvironment(defaultEnv);
}
}
}
/**
* Save current request to collection
*/
function saveCurrentRequest() {
if (!activeEndpoint || !spec) return;
const name = prompt('Request name:', `${activeEndpoint.method} ${activeEndpoint.path}`);
if (!name) return;
const workspace = WorkspaceManager.getCurrentWorkspace();
const collection = workspace.collections[0];
if (!collection) return;
const saved = new WorkspaceManager.SavedRequest(
name,
activeEndpoint.method,
activeEndpoint.path
);
saved.description = activeEndpoint.description || '';
// Gather current parameters
document.querySelectorAll('.param-input').forEach(input => {
const paramIn = input.dataset.paramIn;
const paramName = input.dataset.paramName;
if (paramIn && paramName && input.value) {
saved.params[paramIn][paramName] = input.value;
}
});
// Save body
const bodyInput = document.querySelector('[data-request-body="true"]');
if (bodyInput) {
saved.body = bodyInput.textContent;
}
WorkspaceManager.saveRequestToCollection(workspace.id, collection.id, saved);
alert(`Request saved to "${collection.name}"`);
}
/**
* Keyboard shortcuts and accessibility
*/
function initializeKeyboardShortcuts() {
const shortcuts = {
'/': () => document.getElementById('searchInput')?.focus(),
'KeyS': (e) => e.ctrlKey && saveCurrentRequest(),
'KeyK': (e) => e.ctrlKey && toggleCommandPalette(),
'Tab': (e) => handleTabNavigation(e),
'Escape': () => closeModals()
};
document.addEventListener('keydown', (e) => {
// Don't trigger shortcuts when typing in input fields
if (e.target.matches('input, textarea, [contenteditable]')) {
if (e.key === 'Escape') {
e.target.blur();
}
return;
}
const handler = shortcuts[e.code] || shortcuts[e.key];
if (handler) {
handler(e);
}
});
// Show keyboard shortcut hints
addShortcutHints();
}
/**
* Add visual keyboard shortcut hints to UI
*/
function addShortcutHints() {
const searchInput = document.getElementById('searchInput');
if (searchInput && !searchInput.parentElement.querySelector('.kbd')) {
const kbd = document.createElement('kbd');
kbd.className = 'kbd search-kbd';
kbd.textContent = '/';
searchInput.parentElement.appendChild(kbd);
}
}
/**
* Toggle command palette (future enhancement)
*/
function toggleCommandPalette() {
// Placeholder for future command palette implementation
console.log('Command palette not yet implemented');
}
/**
* Handle Tab navigation for better accessibility
*/
function handleTabNavigation(e) {
const focusableElements = document.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return;
const currentIndex = Array.from(focusableElements).indexOf(document.activeElement);
let nextIndex = e.shiftKey ? currentIndex - 1 : currentIndex + 1;
if (nextIndex >= focusableElements.length) nextIndex = 0;
if (nextIndex < 0) nextIndex = focusableElements.length - 1;
const nextElement = focusableElements[nextIndex];
if (nextElement) {
e.preventDefault();
nextElement.focus();
}
}
/**
* Close all open modals
*/
function closeModals() {
document.querySelectorAll('[role="dialog"], .modal, .drawer').forEach(modal => {
modal.style.display = 'none';
});
}
/**
* Enhance accessibility with ARIA labels
*/
function enhanceAccessibility() {
// Add ARIA roles and labels
const sidebar = document.getElementById('sidebar');
if (sidebar) sidebar.setAttribute('role', 'navigation');
const detailPane = document.getElementById('detailPane');
if (detailPane) detailPane.setAttribute('role', 'main');
const tryPane = document.getElementById('tryPane');
if (tryPane) tryPane.setAttribute('role', 'complementary');
// Add aria-labels to icon buttons
document.querySelectorAll('.icon-btn').forEach(btn => {
if (!btn.getAttribute('aria-label')) {
btn.setAttribute('aria-label', btn.title || btn.textContent);
}
});
// Add roles to custom components
document.querySelectorAll('.param-group-header').forEach(header => {
header.setAttribute('role', 'button');
header.setAttribute('tabindex', '0');
});
document.querySelectorAll('[data-overview], [data-endpoint]').forEach(link => {
link.setAttribute('role', 'menuitem');
});
}
/**
* Add focus visible styles for keyboard navigation
*/
function initializeFocusStyles() {
const style = document.createElement('style');
style.textContent = `
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
button:focus-visible,
a:focus-visible,
input:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
`;
document.head.appendChild(style);
}
/**
* Enhanced error handling and user feedback
*/
function showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.setAttribute('role', 'status');
notification.setAttribute('aria-live', 'polite');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 16px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-1);
z-index: 9999;
animation: slideIn 0.3s ease-out;
`;
document.body.appendChild(notification);
if (duration > 0) {
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => notification.remove(), 300);
}, duration);
}
return notification;
}
/**
* Improved error logging
*/
const originalConsoleError = console.error;
console.error = function(...args) {
originalConsoleError.apply(console, args);
const message = args[0]?.toString?.() || String(args[0]);
if (message && !message.includes('HTTP')) {
showNotification(`Error: ${message.substring(0, 50)}`, 'error', 5000);
}
};
/**
* Initialize all enhancements
*/
function initializeEnhancements() {
initializeWorkspaceManager();
initializeKeyboardShortcuts();
enhanceAccessibility();
initializeFocusStyles();
}
// Call on load
initializeEnhancements();
init(); init();
})(); })();
+407
View File
@@ -0,0 +1,407 @@
/**
* Code Generation Module
* Generates API request code snippets in multiple languages and frameworks
*/
const CodeGenerator = (() => {
"use strict";
/**
* Generate code in specified language
*/
function generate(language, request, options = {}) {
const generator = generators[language.toLowerCase()];
if (!generator) {
return `// Language "${language}" not supported`;
}
return generator(request, options);
}
/**
* Get list of supported languages
*/
function getSupportedLanguages() {
return Object.keys(generators).sort();
}
// Language generators
const generators = {
curl: (req) => generateCurl(req),
fetch: (req) => generateFetch(req),
axios: (req) => generateAxios(req),
httpclient: (req) => generateHttpClient(req),
httppy: (req) => generatePythonHttp(req),
requests: (req) => generatePythonRequests(req),
httpgo: (req) => generateGoHttp(req),
csharp: (req) => generateCSharp(req),
javascript: (req) => generateFetch(req),
typescript: (req) => generateFetch(req),
ruby: (req) => generateRuby(req),
php: (req) => generatePhp(req),
java: (req) => generateJava(req),
kotlin: (req) => generateKotlin(req),
powershell: (req) => generatePowerShell(req),
swift: (req) => generateSwift(req),
};
function escapeString(str) {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
function escapeStringForJson(str) {
return JSON.stringify(str);
}
// ============= cURL =============
function generateCurl(req) {
let cmd = 'curl -X ' + req.method + ' ';
// Headers
Object.entries(req.headers || {}).forEach(([key, value]) => {
cmd += `-H ${escapeStringForJson(key + ': ' + value)} `;
});
// Body
if (req.body) {
cmd += `-d ${escapeStringForJson(req.body)} `;
}
// URL
cmd += escapeStringForJson(req.url);
return cmd;
}
// ============= Fetch API =============
function generateFetch(req) {
let code = `const url = '${escapeString(req.url)}';\n\n`;
code += `const options = {\n`;
code += ` method: '${req.method}',\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += ` headers: {\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
});
code += ` },\n`;
}
if (req.body) {
code += ` body: ${typeof req.body === 'string' && req.body.startsWith('{') ? 'JSON.stringify(' + req.body + ')' : escapeStringForJson(req.body)},\n`;
}
code += `};\n\n`;
code += `try {\n`;
code += ` const response = await fetch(url, options);\n`;
code += ` const data = await response.json();\n`;
code += ` console.log('Success:', data);\n`;
code += `} catch (error) {\n`;
code += ` console.error('Error:', error);\n`;
code += `}`;
return code;
}
// ============= Axios =============
function generateAxios(req) {
let code = `import axios from 'axios';\n\n`;
code += `const config = {\n`;
code += ` method: '${req.method.toLowerCase()}',\n`;
code += ` url: '${escapeString(req.url)}',\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += ` headers: {\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
});
code += ` },\n`;
}
if (req.body) {
code += ` data: ${typeof req.body === 'string' && req.body.startsWith('{') ? req.body : escapeStringForJson(req.body)},\n`;
}
code += `};\n\n`;
code += `axios(config)\n`;
code += ` .then((response) => console.log(response.data))\n`;
code += ` .catch((error) => console.error(error));`;
return code;
}
// ============= Python - Requests =============
function generatePythonRequests(req) {
let code = `import requests\n\n`;
code += `url = '${escapeString(req.url)}'\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += `headers = {\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` '${escapeString(key)}': '${escapeString(String(value))}',\n`;
});
code += `}\n`;
} else {
code += `headers = {}\n`;
}
if (req.body) {
code += `\ndata = ${req.body}\n\n`;
code += `response = requests.${req.method.toLowerCase()}(url, json=data, headers=headers)\n`;
} else {
code += `\nresponse = requests.${req.method.toLowerCase()}(url, headers=headers)\n`;
}
code += `print(response.json())`;
return code;
}
// ============= Python - HTTP =============
function generatePythonHttp(req) {
let code = `import http.client\nimport json\n\n`;
const url = new URL(req.url);
const host = url.hostname;
const path = url.pathname + url.search;
code += `conn = http.client.HTTPSConnection("${host}")\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += `headers = {\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` "${escapeString(key)}": "${escapeString(String(value))}",\n`;
});
code += `}\n`;
} else {
code += `headers = {}\n`;
}
if (req.body) {
code += `\nbody = ${req.body}\n`;
code += `conn.request("${req.method}", "${escapeString(path)}", json.dumps(body), headers)\n`;
} else {
code += `\nconn.request("${req.method}", "${escapeString(path)}", headers=headers)\n`;
}
code += `response = conn.getresponse()\ndata = response.read()\nprint(data.decode())`;
return code;
}
// ============= Go =============
function generateGoHttp(req) {
let code = `package main\n\nimport (\n\t"fmt"\n\t"io"\n\t"net/http"\n)\n\n`;
code += `func main() {\n`;
code += `\treq, _ := http.NewRequest("${req.method}", "${escapeString(req.url)}", nil)\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
code += `\treq.Header.Add("${escapeString(key)}", "${escapeString(String(value))}")\n`;
});
code += `\n\tres, _ := http.DefaultClient.Do(req)\n`;
code += `\tdefer res.Body.Close()\n`;
code += `\tbody, _ := io.ReadAll(res.Body)\n`;
code += `\tfmt.Println(string(body))\n`;
code += `}`;
return code;
}
// ============= C# / .NET =============
function generateCSharp(req) {
let code = `using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\n`;
code += `class Program {\n`;
code += ` static async Task Main() {\n`;
code += ` using (var client = new HttpClient()) {\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
if (key.toLowerCase() !== 'content-type') {
code += ` client.DefaultRequestHeaders.Add("${escapeString(key)}", "${escapeString(String(value))}");\n`;
}
});
if (req.body) {
code += `\n var content = new StringContent("${escapeString(req.body)}", null, "application/json");\n`;
code += ` var response = await client.${req.method.charAt(0) + req.method.slice(1).toLowerCase()}Async("${escapeString(req.url)}", content);\n`;
} else {
code += `\n var response = await client.GetAsync("${escapeString(req.url)}");\n`;
}
code += ` var result = await response.Content.ReadAsStringAsync();\n`;
code += ` Console.WriteLine(result);\n`;
code += ` }\n`;
code += ` }\n`;
code += `}`;
return code;
}
// ============= Ruby =============
function generateRuby(req) {
let code = `require 'net/http'\nrequire 'json'\n\n`;
code += `url = URI('${escapeString(req.url)}')\n`;
code += `http = Net::HTTP.new(url.host, url.port)\n`;
code += `http.use_ssl = true\n\n`;
const methodMap = { 'GET': 'Get', 'POST': 'Post', 'PUT': 'Put', 'PATCH': 'Patch', 'DELETE': 'Delete' };
const method = methodMap[req.method] || 'Get';
code += `request = Net::HTTP::${method}.new(url.request_uri)\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
code += `request['${escapeString(key)}'] = '${escapeString(String(value))}'\n`;
});
if (req.body) {
code += `request.body = ${req.body}\n`;
}
code += `\nresponse = http.request(request)\nputs response.body`;
return code;
}
// ============= PHP =============
function generatePhp(req) {
let code = `<?php\n\n$url = '${escapeString(req.url)}';\n`;
code += `$method = '${req.method}';\n\n`;
code += `$options = array(\n`;
code += ` 'http' => array(\n`;
code += ` 'method' => $method,\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += ` 'header' => array(\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` '${escapeString(key)}: ${String(value)}',\n`;
});
code += ` ),\n`;
}
if (req.body) {
code += ` 'content' => '${escapeString(req.body)}',\n`;
}
code += ` )\n);\n\n`;
code += `$context = stream_context_create($options);\n`;
code += `$response = file_get_contents($url, false, $context);\n`;
code += `echo $response;\n\n?>`;
return code;
}
// ============= Java =============
function generateJava(req) {
let code = `import java.net.http.HttpClient;\n`;
code += `import java.net.http.HttpRequest;\n`;
code += `import java.net.http.HttpResponse;\n`;
code += `import java.net.URI;\n\n`;
code += `public class ApiRequest {\n`;
code += ` public static void main(String[] args) throws Exception {\n`;
code += ` HttpClient client = HttpClient.newHttpClient();\n\n`;
code += ` HttpRequest request = HttpRequest.newBuilder()\n`;
code += ` .uri(URI.create("${escapeString(req.url)}"))\n`;
code += ` .method("${req.method}", HttpRequest.BodyPublishers.noBody())\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
code += ` .header("${escapeString(key)}", "${escapeString(String(value))}")\n`;
});
code += ` .build();\n\n`;
code += ` HttpResponse<String> response = client.send(request,\n`;
code += ` HttpResponse.BodyHandlers.ofString());\n\n`;
code += ` System.out.println(response.body());\n`;
code += ` }\n`;
code += `}`;
return code;
}
// ============= Kotlin =============
function generateKotlin(req) {
let code = `import java.net.HttpURLConnection\nimport java.net.URL\n\n`;
code += `fun main() {\n`;
code += ` val url = URL("${escapeString(req.url)}")\n`;
code += ` val connection = url.openConnection() as HttpURLConnection\n`;
code += ` connection.requestMethod = "${req.method}"\n\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
code += ` connection.setRequestProperty("${escapeString(key)}", "${escapeString(String(value))}")\n`;
});
code += `\n val inputStream = connection.inputStream\n`;
code += ` val response = inputStream.bufferedReader().use { it.readText() }\n`;
code += ` println(response)\n`;
code += `}`;
return code;
}
// ============= PowerShell =============
function generatePowerShell(req) {
let code = `$uri = "${escapeString(req.url)}"\n`;
code += `$method = "${req.method}"\n`;
if (Object.keys(req.headers || {}).length > 0) {
code += `$headers = @{\n`;
Object.entries(req.headers).forEach(([key, value]) => {
code += ` "${escapeString(key)}" = "${escapeString(String(value))}"\n`;
});
code += `}\n\n`;
}
if (req.body) {
code += `$body = '${escapeString(req.body)}'\n`;
code += `$response = Invoke-WebRequest -Uri $uri -Method $method -Headers $headers -Body $body\n`;
} else {
code += `$response = Invoke-WebRequest -Uri $uri -Method $method`;
if (Object.keys(req.headers || {}).length > 0) {
code += ` -Headers $headers`;
}
code += `\n`;
}
code += `$response.Content`;
return code;
}
// ============= Swift =============
function generateSwift(req) {
let code = `import Foundation\n\nlet url = URL(string: "${escapeString(req.url)}")!\n`;
code += `var request = URLRequest(url: url)\n`;
code += `request.httpMethod = "${req.method}"\n\n`;
Object.entries(req.headers || {}).forEach(([key, value]) => {
code += `request.setValue("${escapeString(String(value))}", forHTTPHeaderField: "${escapeString(key)}")\n`;
});
if (req.body) {
code += `\nrequest.httpBody = "${escapeString(req.body)}".data(using: .utf8)\n`;
}
code += `\nlet task = URLSession.shared.dataTask(with: request) { data, response, error in\n`;
code += ` if let data = data {\n`;
code += ` let json = String(data: data, encoding: .utf8)!\n`;
code += ` print(json)\n`;
code += ` }\n`;
code += `}\n`;
code += `task.resume()`;
return code;
}
// Public API
return {
generate,
getSupportedLanguages,
escapeString
};
})();
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = CodeGenerator;
}
+5
View File
@@ -115,6 +115,11 @@
<input id="importFile" type="file" accept=".json" hidden /> <input id="importFile" type="file" accept=".json" hidden />
<input id="diffFile" type="file" accept=".json" hidden /> <input id="diffFile" type="file" accept=".json" hidden />
<!-- New request builder, code generator, and workspace manager modules -->
<script src="request-builder.js"></script>
<script src="code-generator.js"></script>
<script src="workspace-manager.js"></script>
<script>window.__DOXA_API_SPEC_URL__ = "__DOXA_API_SPEC_PATH__";</script> <script>window.__DOXA_API_SPEC_URL__ = "__DOXA_API_SPEC_PATH__";</script>
<script src="app.js"></script> <script src="app.js"></script>
</body> </body>
+399
View File
@@ -0,0 +1,399 @@
/**
* Advanced Request Builder Module
* Handles parameter composition, validation, defaults, and intelligent request building
*/
const RequestBuilder = (() => {
"use strict";
/**
* Parameter state management
*/
class ParameterState {
constructor() {
this.path = {};
this.query = {};
this.header = {};
this.cookie = {};
this.body = null;
this.bodyType = 'json'; // json, form, raw, binary
this.formData = {};
}
clone() {
const copy = new ParameterState();
copy.path = { ...this.path };
copy.query = { ...this.query };
copy.header = { ...this.header };
copy.cookie = { ...this.cookie };
copy.body = this.body;
copy.bodyType = this.bodyType;
copy.formData = { ...this.formData };
return copy;
}
clear() {
this.path = {};
this.query = {};
this.header = {};
this.cookie = {};
this.body = null;
this.bodyType = 'json';
this.formData = {};
}
}
/**
* Build request object from endpoint definition and parameter state
*/
function buildRequest(endpoint, paramState, baseUrl) {
const req = {
method: endpoint.method,
path: resolvePath(endpoint.path, paramState.path),
url: buildUrl(baseUrl, endpoint.path, paramState),
headers: buildHeaders(endpoint, paramState),
query: paramState.query,
body: buildBody(endpoint, paramState),
cookies: paramState.cookie
};
return req;
}
/**
* Resolve path parameters
*/
function resolvePath(pathTemplate, pathParams) {
let path = pathTemplate;
Object.entries(pathParams).forEach(([key, value]) => {
// Encode URI component but preserve slashes in path
path = path.replace(`:${key}`, encodeURIComponent(String(value || '')));
path = path.replace(`{${key}}`, encodeURIComponent(String(value || '')));
});
return path;
}
/**
* Build complete URL with query parameters
*/
function buildUrl(baseUrl, path, paramState) {
let resolvedPath = resolvePath(path, paramState.path);
let url = baseUrl.replace(/\/$/, '') + resolvedPath;
const queryParams = new URLSearchParams();
Object.entries(paramState.query).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
queryParams.append(key, String(value));
}
});
const qs = queryParams.toString();
if (qs) url += '?' + qs;
return url;
}
/**
* Build headers including auth, content-type, etc
*/
function buildHeaders(endpoint, paramState) {
const headers = {};
// User-provided headers
Object.entries(paramState.header).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
headers[key] = String(value);
}
});
// Auto content-type based on body
if (paramState.body !== null && paramState.body !== undefined) {
if (paramState.bodyType === 'json') {
headers['Content-Type'] = 'application/json';
} else if (paramState.bodyType === 'form') {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
}
// Copy cookies to header if not already set
if (Object.keys(paramState.cookie).length > 0) {
const cookieStr = Object.entries(paramState.cookie)
.filter(([, v]) => v != null && v !== '')
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('; ');
if (cookieStr) headers['Cookie'] = cookieStr;
}
return headers;
}
/**
* Build request body
*/
function buildBody(endpoint, paramState) {
if (!paramState.body) return null;
if (paramState.bodyType === 'json') {
if (typeof paramState.body === 'string') {
try {
// Validate JSON
JSON.parse(paramState.body);
return paramState.body;
} catch (e) {
return null; // Invalid JSON
}
}
return JSON.stringify(paramState.body);
}
if (paramState.bodyType === 'form') {
const params = new URLSearchParams();
Object.entries(paramState.formData).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, String(value));
}
});
return params.toString();
}
return paramState.body;
}
/**
* Validate parameters against endpoint schema
*/
function validateParameters(endpoint, paramState) {
const errors = [];
const warnings = [];
if (!endpoint.parameters) return { errors, warnings };
endpoint.parameters.forEach((param) => {
const value = paramState[param.in]?.[param.name];
const isEmpty = value === undefined || value === null || value === '';
// Check required
if (param.required && isEmpty) {
errors.push(`Parameter '${param.name}' is required`);
}
// Validate type and format
if (!isEmpty) {
const validation = validateParameterValue(value, param);
if (!validation.valid) {
errors.push(`Parameter '${param.name}': ${validation.message}`);
}
}
// Check for deprecated params
if (param.deprecated) {
warnings.push(`Parameter '${param.name}' is deprecated`);
}
});
return { errors, warnings };
}
/**
* Validate a parameter value against its schema
*/
function validateParameterValue(value, param) {
const strValue = String(value);
// Validate type
if (param.schema?.type) {
switch (param.schema.type) {
case 'integer':
if (!/^-?\d+$/.test(strValue)) {
return { valid: false, message: `must be an integer` };
}
if (param.schema.minimum !== undefined && parseInt(strValue) < param.schema.minimum) {
return { valid: false, message: `must be >= ${param.schema.minimum}` };
}
if (param.schema.maximum !== undefined && parseInt(strValue) > param.schema.maximum) {
return { valid: false, message: `must be <= ${param.schema.maximum}` };
}
break;
case 'number':
if (!/^-?\d+(\.\d+)?$/.test(strValue)) {
return { valid: false, message: `must be a number` };
}
break;
case 'boolean':
if (!['true', 'false', '1', '0'].includes(strValue.toLowerCase())) {
return { valid: false, message: `must be true or false` };
}
break;
case 'string':
if (param.schema.minLength !== undefined && strValue.length < param.schema.minLength) {
return { valid: false, message: `must be at least ${param.schema.minLength} characters` };
}
if (param.schema.maxLength !== undefined && strValue.length > param.schema.maxLength) {
return { valid: false, message: `must be at most ${param.schema.maxLength} characters` };
}
if (param.schema.pattern && !new RegExp(param.schema.pattern).test(strValue)) {
return { valid: false, message: `invalid format` };
}
break;
}
}
// Validate enum
if (param.schema?.enum && !param.schema.enum.includes(value)) {
return { valid: false, message: `must be one of: ${param.schema.enum.join(', ')}` };
}
return { valid: true };
}
/**
* Fill parameters from examples or defaults
*/
function populateDefaults(endpoint, paramState) {
if (!endpoint.parameters) return;
endpoint.parameters.forEach((param) => {
if (!paramState[param.in]) paramState[param.in] = {};
// Skip if already set
if (paramState[param.in][param.name] !== undefined && paramState[param.in][param.name] !== null) {
return;
}
// Try example
if (param.example !== undefined) {
paramState[param.in][param.name] = param.example;
return;
}
// Try schema default
if (param.schema?.default !== undefined) {
paramState[param.in][param.name] = param.schema.default;
return;
}
// Provide smart defaults based on type
if (param.schema?.type) {
switch (param.schema.type) {
case 'integer':
paramState[param.in][param.name] = param.schema.minimum || 1;
break;
case 'number':
paramState[param.in][param.name] = param.schema.minimum || 0.0;
break;
case 'boolean':
paramState[param.in][param.name] = true;
break;
case 'string':
paramState[param.in][param.name] = '';
break;
}
}
});
}
/**
* Generate example body from schema
*/
function generateExampleBody(schema, schemas) {
if (!schema) return null;
// Handle references
if (schema.$ref) {
const refName = schema.$ref.split('/').pop();
schema = schemas?.[refName] || schema;
}
if (schema.type === 'object') {
const example = {};
if (schema.properties) {
Object.entries(schema.properties).forEach(([key, propSchema]) => {
if (propSchema.example !== undefined) {
example[key] = propSchema.example;
} else if (propSchema.type === 'string') {
example[key] = `<${key}>`;
} else if (propSchema.type === 'integer') {
example[key] = propSchema.minimum || 1;
} else if (propSchema.type === 'number') {
example[key] = propSchema.minimum || 0.0;
} else if (propSchema.type === 'boolean') {
example[key] = true;
} else if (propSchema.type === 'array') {
example[key] = [generateExampleBody(propSchema.items, schemas)];
}
});
}
return example;
}
if (schema.type === 'array') {
return [generateExampleBody(schema.items, schemas)];
}
if (schema.example !== undefined) return schema.example;
switch (schema.type) {
case 'string': return 'example';
case 'integer': return 0;
case 'number': return 0.0;
case 'boolean': return true;
default: return null;
}
}
/**
* Extract suggestion data from response to prefill next request
*/
function extractSuggestions(response, responseSchema) {
const suggestions = {};
if (!response || typeof response !== 'object') return suggestions;
if (responseSchema?.type === 'object' && responseSchema.properties) {
Object.keys(responseSchema.properties).forEach((key) => {
if (response[key] !== undefined) {
suggestions[key] = response[key];
}
});
}
return suggestions;
}
/**
* Check if request is ready to send
*/
function isRequestReady(endpoint, paramState) {
if (!endpoint) return false;
if (!endpoint.parameters) return true;
const hasErrors = endpoint.parameters.some((param) => {
if (!param.required) return false;
const value = paramState[param.in]?.[param.name];
return value === undefined || value === null || value === '';
});
return !hasErrors;
}
// Public API
return {
ParameterState,
buildRequest,
resolvePath,
buildUrl,
buildHeaders,
buildBody,
validateParameters,
validateParameterValue,
populateDefaults,
generateExampleBody,
extractSuggestions,
isRequestReady
};
})();
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = RequestBuilder;
}
+435
View File
@@ -0,0 +1,435 @@
/**
* Workspace Manager Module
* Handles request collections, workspaces, environments, and persistence
*/
const WorkspaceManager = (() => {
"use strict";
const STORAGE_KEY = 'DoxaApi.workspaces';
const CURRENT_WS_KEY = 'DoxaApi.currentWorkspace';
const ENV_KEY = 'DoxaApi.environments';
/**
* Workspace structure
*/
class Workspace {
constructor(name = 'Default') {
this.id = generateId();
this.name = name;
this.description = '';
this.collections = [];
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
}
/**
* Collection structure (folders for organizing requests)
*/
class Collection {
constructor(name = 'New Collection') {
this.id = generateId();
this.name = name;
this.description = '';
this.requests = [];
this.folders = [];
this.createdAt = Date.now();
}
}
/**
* Request in a collection
*/
class SavedRequest {
constructor(name = 'New Request', method = 'GET', path = '') {
this.id = generateId();
this.name = name;
this.method = method;
this.path = path;
this.description = '';
this.tags = [];
this.params = {
path: {},
query: {},
header: {},
cookie: {}
};
this.body = null;
this.bodyType = 'json';
this.formData = {};
this.auth = null;
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
clone() {
const copy = new SavedRequest(this.name, this.method, this.path);
copy.id = generateId();
copy.description = this.description;
copy.tags = [...this.tags];
copy.params = JSON.parse(JSON.stringify(this.params));
copy.body = this.body;
copy.bodyType = this.bodyType;
copy.formData = { ...this.formData };
copy.auth = this.auth;
return copy;
}
}
/**
* Environment for variable substitution
*/
class Environment {
constructor(name = 'Default') {
this.id = generateId();
this.name = name;
this.description = '';
this.variables = {};
this.isActive = false;
this.createdAt = Date.now();
}
}
/**
* Generate unique ID
*/
function generateId() {
return 'id_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
/**
* Get all workspaces
*/
function getWorkspaces() {
try {
const data = localStorage.getItem(STORAGE_KEY);
return data ? JSON.parse(data).map(deserializeWorkspace) : [];
} catch (e) {
console.error('Failed to load workspaces:', e);
return [];
}
}
/**
* Get current active workspace
*/
function getCurrentWorkspace() {
const workspaces = getWorkspaces();
if (workspaces.length === 0) {
const ws = new Workspace('Default');
saveWorkspace(ws);
return ws;
}
return workspaces[0];
}
/**
* Save workspace
*/
function saveWorkspace(workspace) {
workspace.updatedAt = Date.now();
const workspaces = getWorkspaces();
const index = workspaces.findIndex(w => w.id === workspace.id);
if (index >= 0) {
workspaces[index] = workspace;
} else {
workspaces.push(workspace);
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces));
return workspace;
}
/**
* Delete workspace
*/
function deleteWorkspace(workspaceId) {
let workspaces = getWorkspaces();
workspaces = workspaces.filter(w => w.id !== workspaceId);
localStorage.setItem(STORAGE_KEY, JSON.stringify(workspaces));
}
/**
* Add collection to workspace
*/
function addCollection(workspaceId, collection) {
const workspace = getWorkspaces().find(w => w.id === workspaceId);
if (workspace) {
workspace.collections.push(collection);
saveWorkspace(workspace);
}
return collection;
}
/**
* Save request to collection
*/
function saveRequestToCollection(workspaceId, collectionId, request) {
const workspace = getWorkspaces().find(w => w.id === workspaceId);
if (!workspace) return null;
const collection = findCollectionRecursive(workspace.collections, collectionId);
if (!collection) return null;
request.updatedAt = Date.now();
const index = collection.requests.findIndex(r => r.id === request.id);
if (index >= 0) {
collection.requests[index] = request;
} else {
collection.requests.push(request);
}
saveWorkspace(workspace);
return request;
}
/**
* Find collection recursively in folder structure
*/
function findCollectionRecursive(collections, collectionId) {
for (const col of collections) {
if (col.id === collectionId) return col;
if (col.folders) {
const found = findCollectionRecursive(col.folders, collectionId);
if (found) return found;
}
}
return null;
}
/**
* Get all requests in workspace
*/
function getAllRequests(workspaceId) {
const workspace = getWorkspaces().find(w => w.id === workspaceId);
if (!workspace) return [];
const requests = [];
function traverse(collections) {
collections.forEach(col => {
requests.push(...(col.requests || []));
if (col.folders) {
traverse(col.folders);
}
});
}
traverse(workspace.collections);
return requests;
}
/**
* Search requests in workspace
*/
function searchRequests(workspaceId, query) {
const allRequests = getAllRequests(workspaceId);
const lowerQuery = query.toLowerCase();
return allRequests.filter(req =>
req.name.toLowerCase().includes(lowerQuery) ||
req.path.toLowerCase().includes(lowerQuery) ||
req.description.toLowerCase().includes(lowerQuery) ||
(req.tags && req.tags.some(t => t.toLowerCase().includes(lowerQuery)))
);
}
/**
* Get environments
*/
function getEnvironments() {
try {
const data = localStorage.getItem(ENV_KEY);
return data ? JSON.parse(data).map(deserializeEnvironment) : [];
} catch (e) {
console.error('Failed to load environments:', e);
return [];
}
}
/**
* Save environment
*/
function saveEnvironment(environment) {
const environments = getEnvironments();
const index = environments.findIndex(e => e.id === environment.id);
if (index >= 0) {
environments[index] = environment;
} else {
environments.push(environment);
}
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
return environment;
}
/**
* Delete environment
*/
function deleteEnvironment(environmentId) {
let environments = getEnvironments();
environments = environments.filter(e => e.id !== environmentId);
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
}
/**
* Get active environment
*/
function getActiveEnvironment() {
const environments = getEnvironments();
return environments.find(e => e.isActive) || null;
}
/**
* Set active environment
*/
function setActiveEnvironment(environmentId) {
const environments = getEnvironments();
environments.forEach(e => e.isActive = e.id === environmentId);
localStorage.setItem(ENV_KEY, JSON.stringify(environments));
}
/**
* Substitute variables in string
*/
function substituteVariables(str, environment) {
if (!str || !environment) return str;
let result = str;
Object.entries(environment.variables).forEach(([name, value]) => {
result = result.replace(new RegExp(`\\$\\{${name}\\}|\\$${name}\\b`, 'g'), String(value || ''));
});
return result;
}
/**
* Export workspace as JSON
*/
function exportWorkspace(workspaceId) {
const workspace = getWorkspaces().find(w => w.id === workspaceId);
if (!workspace) return null;
return {
version: '1.0',
exported: new Date().toISOString(),
workspace: workspace,
environments: getEnvironments()
};
}
/**
* Import workspace from JSON
*/
function importWorkspace(json) {
try {
const data = typeof json === 'string' ? JSON.parse(json) : json;
// Validate version
if (!data.version) throw new Error('Invalid format: missing version');
// Create new workspace with imported data
const workspace = data.workspace;
workspace.id = generateId();
workspace.createdAt = Date.now();
workspace.updatedAt = Date.now();
// Import environments
if (data.environments && Array.isArray(data.environments)) {
data.environments.forEach(env => {
env.id = generateId();
saveEnvironment(env);
});
}
// Save workspace
return saveWorkspace(workspace);
} catch (e) {
console.error('Failed to import workspace:', e);
return null;
}
}
/**
* Duplicate request
*/
function duplicateRequest(request) {
const duplicate = request.clone();
duplicate.name = `${request.name} (copy)`;
return duplicate;
}
/**
* Duplicate collection
*/
function duplicateCollection(collection) {
const duplicate = new Collection(collection.name + ' (copy)');
duplicate.description = collection.description;
duplicate.requests = collection.requests.map(r => r.clone ? r.clone() : r);
if (collection.folders) {
duplicate.folders = collection.folders.map(f => duplicateCollection(f));
}
return duplicate;
}
/**
* Get recent requests
*/
function getRecentRequests(workspaceId, limit = 10) {
return getAllRequests(workspaceId)
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
.slice(0, limit);
}
/**
* Deserialize workspace (helper for JSON parsing)
*/
function deserializeWorkspace(data) {
const ws = Object.assign(new Workspace(data.name), data);
return ws;
}
/**
* Deserialize environment (helper for JSON parsing)
*/
function deserializeEnvironment(data) {
const env = Object.assign(new Environment(data.name), data);
return env;
}
// Public API
return {
Workspace,
Collection,
SavedRequest,
Environment,
generateId,
getWorkspaces,
getCurrentWorkspace,
saveWorkspace,
deleteWorkspace,
addCollection,
saveRequestToCollection,
getAllRequests,
searchRequests,
getEnvironments,
saveEnvironment,
deleteEnvironment,
getActiveEnvironment,
setActiveEnvironment,
substituteVariables,
exportWorkspace,
importWorkspace,
duplicateRequest,
duplicateCollection,
getRecentRequests
};
})();
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = WorkspaceManager;
}