(function() { var MAX_FILE_BYTES = 256 * 1024; var state = { workspaceId: null, onImported: null, file: null, jobId: null, preview: null, filterQuery: '', filterMethod: '', revision: 0, previewController: null, applyController: null, applyInFlight: false, restoreFocus: null, }; function qs(id) { return document.getElementById(id); } function tKey(key) { return window.t ? window.t(key) : key; } function tfKey(key, vars) { return window.tf ? window.tf(key, vars) : tKey(key); } function locale() { return localStorage.getItem('crank_lang') === 'ru' ? 'ru' : 'en'; } function isCurrent(revision, workspaceId, language) { return revision === state.revision && workspaceId === state.workspaceId && language === locale() && !qs('openapi-import-modal').hidden; } function setStatus(message, isError, focus) { var node = qs('openapi-import-status'); if (!node) return; node.textContent = message || ''; node.classList.toggle('error', !!isError); if (focus && message) { node.focus(); } } function boundedFileName(file) { var name = String(file && file.name || ''); return name.length > 128 ? name.slice(0, 127) + '…' : name; } function formatSize(bytes) { var language = locale() === 'ru' ? 'ru-RU' : 'en-US'; return new Intl.NumberFormat(language).format(bytes) + ' B'; } function renderFileName() { var node = qs('openapi-import-file-name'); if (!node) return; node.textContent = state.file ? boundedFileName(state.file) + ' · ' + formatSize(state.file.size) : tKey('openapi.file.none'); } function selectedKeys() { return Array.from(document.querySelectorAll('[data-openapi-operation]:checked')) .map(function(input) { return input.value; }); } function operationRows() { return Array.from(document.querySelectorAll('.openapi-import-operation')); } function visibleOperationRows() { return operationRows().filter(function(row) { return !row.hidden; }); } function updateSelection() { var node = qs('openapi-import-selection'); var selectedCount = selectedKeys().length; var totalCount = document.querySelectorAll('[data-openapi-operation]').length; var visibleCount = visibleOperationRows().length; if (node) { node.textContent = tfKey('openapi.selection', { selected: selectedCount, total: totalCount, visible: visibleCount, }); } document.querySelectorAll('.openapi-import-group').forEach(function(groupNode) { var rows = Array.from(groupNode.querySelectorAll('.openapi-import-operation')); var visibleInputs = rows .filter(function(row) { return !row.hidden; }) .map(function(row) { return row.querySelector('[data-openapi-operation]'); }) .filter(Boolean); var selectedVisible = visibleInputs.filter(function(input) { return input.checked; }).length; var selectedTotal = rows.filter(function(row) { var input = row.querySelector('[data-openapi-operation]'); return input && input.checked; }).length; var checkbox = groupNode.querySelector('[data-openapi-group]'); var counter = groupNode.querySelector('[data-openapi-group-count]'); if (checkbox) { checkbox.indeterminate = selectedVisible > 0 && selectedVisible < visibleInputs.length; checkbox.checked = visibleInputs.length > 0 && selectedVisible === visibleInputs.length; } if (counter) { counter.textContent = tfKey('openapi.group.selection', { selected: selectedTotal, total: rows.length, visible: visibleInputs.length, }); } }); } function applyFilters() { var query = state.filterQuery.toLowerCase(); var method = state.filterMethod.toLowerCase(); document.querySelectorAll('.openapi-import-group').forEach(function(groupNode) { var visibleInGroup = 0; groupNode.querySelectorAll('.openapi-import-operation').forEach(function(row) { var matchesMethod = !method || String(row.dataset.openapiMethod).toLowerCase() === method; var matchesQuery = !query || String(row.dataset.openapiSearch).toLowerCase().indexOf(query) >= 0; var visible = matchesMethod && matchesQuery; row.hidden = !visible; if (visible) visibleInGroup += 1; }); groupNode.hidden = visibleInGroup === 0; }); updateSelection(); } function setVisibleSelection(checked) { visibleOperationRows().forEach(function(row) { var input = row.querySelector('[data-openapi-operation]'); if (input) input.checked = checked; }); updateSelection(); } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderFinding(finding) { var severity = String(finding && finding.severity || 'warning').toLowerCase(); var label = severity === 'info' ? 'i' : severity === 'error' ? '×' : '!'; return '' + '' + label + ' ' + escapeHtml(findingMessage(finding)) + ''; } function findingMessage(finding) { var code = String(finding && finding.code || '').replace(/^.*\./, ''); var known = { missing_servers: 'openapi.finding.missing_servers', multiple_servers: 'openapi.finding.multiple_servers', unsupported_request_body: 'openapi.finding.unsupported_request_body', operation_name_renamed: 'openapi.finding.operation_name_renamed', operation_name_conflict: 'openapi.finding.operation_name_conflict', weak_tool_description: 'openapi.finding.weak_tool_description', }; return tKey(known[code] || 'openapi.finding.generic'); } function appendMappingGroup(root, label, values) { if (!values || !values.length) return; var group = document.createElement('div'); group.className = 'openapi-import-mapping-group'; var title = document.createElement('span'); title.className = 'openapi-import-mapping-label'; title.textContent = label; group.appendChild(title); values.slice(0, 8).forEach(function(value) { var chip = document.createElement('code'); chip.className = 'openapi-import-mapping-chip'; chip.textContent = value; group.appendChild(chip); }); if (values.length > 8) { var more = document.createElement('span'); more.className = 'openapi-import-mapping-more'; more.textContent = '+' + (values.length - 8); group.appendChild(more); } root.appendChild(group); } function renderOperationMappingPreview(operation) { var details = document.createElement('div'); var request = { path: [], query: [], headers: [], body: [] }; var inputRules = operation && operation.draft && operation.draft.input_mapping && Array.isArray(operation.draft.input_mapping.rules) ? operation.draft.input_mapping.rules : []; var outputRules = operation && operation.draft && operation.draft.output_mapping && Array.isArray(operation.draft.output_mapping.rules) ? operation.draft.output_mapping.rules : []; details.className = 'openapi-import-mapping-preview'; inputRules.forEach(function(rule) { var target = String(rule.target || ''); if (target.indexOf('$.request.path.') === 0) { request.path.push(target.slice(15)); } else if (target.indexOf('$.request.query.') === 0) { request.query.push(target.slice(16)); } else if (target.indexOf('$.request.headers.') === 0) { request.headers.push(target.slice(18)); } else if (target.indexOf('$.request.body.') === 0) { request.body.push(target.slice(15)); } else if (target === '$.request.body') { request.body.push('body'); } }); appendMappingGroup(details, tKey('openapi.mapping.path'), request.path); appendMappingGroup(details, tKey('openapi.mapping.query'), request.query); appendMappingGroup(details, tKey('openapi.mapping.header'), request.headers); appendMappingGroup(details, tKey('openapi.mapping.body'), request.body); appendMappingGroup(details, tKey('openapi.mapping.response'), outputRules.map(function(rule) { var target = String(rule.target || '').replace('$.output.', '').replace('$.output', 'result'); var source = String(rule.source || '').replace('$.response.body.', '').replace('$.response.body', 'body'); return target + ' ← ' + source; })); return details; } function populateServers(preview) { var serverSelect = qs('openapi-import-server'); var servers = preview.source && preview.source.servers && preview.source.servers.length ? preview.source.servers : ['']; serverSelect.replaceChildren(); servers.forEach(function(server) { var option = document.createElement('option'); option.value = server; option.textContent = server || tKey('openapi.server.later'); serverSelect.appendChild(option); }); } function populateMethodFilter(preview) { var methodFilter = qs('openapi-import-method-filter'); var all = document.createElement('option'); var methods = Array.from(new Set((preview.groups || []).flatMap(function(group) { return (group.operations || []).map(function(operation) { return operation.method; }); }))).sort(); methodFilter.replaceChildren(); all.value = ''; all.textContent = tKey('openapi.method.all'); methodFilter.appendChild(all); methods.forEach(function(method) { var option = document.createElement('option'); option.value = method; option.textContent = method; methodFilter.appendChild(option); }); } function renderPreview(response) { var preview = response.preview; var count = (preview.groups || []).reduce(function(sum, group) { return sum + (group.operations || []).length; }, 0); var source = qs('openapi-import-source'); var groupsNode = qs('openapi-import-groups'); state.jobId = response.job_id; state.preview = preview; state.filterQuery = ''; state.filterMethod = ''; qs('openapi-import-preview-panel').hidden = false; qs('openapi-import-result').hidden = true; qs('openapi-import-result').replaceChildren(); source.textContent = tfKey('openapi.source.summary', { name: boundedFileName(state.file), size: formatSize(state.file.size), count: count, }); qs('openapi-import-search').value = ''; populateServers(preview); populateMethodFilter(preview); groupsNode.replaceChildren(); if (preview.findings && preview.findings.length) { var documentFindings = document.createElement('div'); documentFindings.className = 'openapi-import-document-findings'; documentFindings.innerHTML = preview.findings.map(renderFinding).join(''); groupsNode.appendChild(documentFindings); } (preview.groups || []).forEach(function(group) { var groupNode = document.createElement('section'); var header = document.createElement('div'); groupNode.className = 'openapi-import-group'; header.className = 'openapi-import-group-header'; header.innerHTML = '' + escapeHtml(group.title) + ' · ' + group.operations.length + '' + '' + ''; groupNode.appendChild(header); group.operations.forEach(function(operation) { var row = document.createElement('label'); row.className = 'openapi-import-operation'; row.dataset.openapiMethod = operation.method; row.dataset.openapiSearch = [ group.title, operation.key, operation.suggested_name, operation.suggested_display_name, operation.path, operation.method, ].join(' '); row.innerHTML = '' + '
' + escapeHtml(operation.suggested_display_name) + '
' + '
' + escapeHtml(operation.suggested_name) + ' · ' + escapeHtml(operation.path) + ' · ' + escapeHtml(tfKey('openapi.operation.fields', { input: operation.input_fields, output: operation.output_fields })) + '
' + escapeHtml(operation.method) + ''; row.appendChild(renderOperationMappingPreview(operation)); if (operation.findings && operation.findings.length) { var findings = document.createElement('div'); findings.className = 'openapi-import-findings'; findings.innerHTML = operation.findings.map(renderFinding).join(''); row.appendChild(findings); } groupNode.appendChild(row); }); groupNode.querySelector('[data-openapi-group]').addEventListener('change', function(event) { groupNode.querySelectorAll('.openapi-import-operation').forEach(function(row) { var input = row.querySelector('[data-openapi-operation]'); if (!row.hidden && input) input.checked = event.target.checked; }); updateSelection(); }); groupsNode.appendChild(groupNode); }); groupsNode.querySelectorAll('[data-openapi-operation]').forEach(function(input) { input.addEventListener('change', updateSelection); }); applyFilters(); } function validateFile(file) { if (!file) return 'openapi.error.file_required'; var name = String(file.name || '').toLowerCase(); var yaml = /\.(yaml|yml)$/.test(name); var json = /\.json$/.test(name); if (!yaml && !json) return 'openapi.error.file_type'; if (!file.size) return 'openapi.error.file_empty'; if (file.size > MAX_FILE_BYTES) return 'openapi.error.file_large'; var type = String(file.type || '').toLowerCase(); if (!type || type === 'application/octet-stream') return null; var allowed = yaml ? ['application/yaml', 'application/x-yaml', 'text/yaml', 'text/x-yaml'] : ['application/json', 'application/openapi+json']; return allowed.indexOf(type) >= 0 ? null : 'openapi.error.file_type'; } function abort(controller) { if (controller) controller.abort(); } function clearPreview() { state.jobId = null; state.preview = null; state.filterQuery = ''; state.filterMethod = ''; qs('openapi-import-preview-panel').hidden = true; qs('openapi-import-groups').replaceChildren(); qs('openapi-import-result').hidden = true; qs('openapi-import-result').replaceChildren(); updateSelection(); } function clearServerSelection() { qs('openapi-import-server-custom').value = ''; qs('openapi-import-server').replaceChildren(); } function invalidate(options) { state.revision += 1; abort(state.previewController); abort(state.applyController); state.previewController = null; state.applyController = null; state.applyInFlight = false; qs('openapi-import-preview').disabled = false; qs('openapi-import-create').disabled = false; qs('openapi-import-cancel').hidden = true; clearPreview(); if (options && options.clearFile) { state.file = null; qs('openapi-import-file').value = ''; renderFileName(); } } function showRetry(show) { qs('openapi-import-retry').hidden = !show; } function cancelCurrent() { if (!state.previewController && !state.applyController) return; invalidate(); showRetry(!!state.file); setStatus(tKey('openapi.status.cancelled')); } function errorStatus(error, fallback) { var known = { 'openapi_upload.missing_file': 'openapi.error.file_required', 'openapi_upload.invalid_media_type': 'openapi.error.file_type', 'openapi_upload.empty_file': 'openapi.error.file_empty', 'openapi_upload.file_too_large': 'openapi.error.file_large', 'openapi_upload.no_methods': 'openapi.error.no_methods', 'openapi_upload.malformed_multipart': 'openapi.error.malformed_multipart', 'openapi_upload.invalid_utf8': 'openapi.error.invalid_utf8', 'openapi_upload.invalid_document': 'openapi.error.invalid_document', 'openapi_upload.parser_unavailable': 'openapi.error.parser_unavailable', 'openapi_upload.storage_unavailable': 'openapi.error.storage_unavailable', 'openapi_upload.source_integrity': 'openapi.error.source_integrity', 'openapi_upload.source_unavailable': 'openapi.error.source_unavailable', }; var message = tKey(known[error && error.code] || fallback); var identifiers = []; if (error && error.requestId) { identifiers.push(tfKey('openapi.correlation.request_id', { id: error.requestId })); } if (error && error.traceId) { identifiers.push(tfKey('openapi.correlation.trace_id', { id: error.traceId })); } return identifiers.length ? message + ' ' + identifiers.join(' · ') : message; } async function preview() { var localError = validateFile(state.file); if (localError) { setStatus(tKey(localError), true, true); showRetry(false); return; } if (!state.workspaceId) { setStatus(tKey('openapi.error.workspace'), true, true); return; } invalidate(); var revision = state.revision; var workspaceId = state.workspaceId; var language = locale(); var controller = new AbortController(); state.previewController = controller; qs('openapi-import-preview').disabled = true; qs('openapi-import-cancel').hidden = false; showRetry(false); setStatus(tKey('openapi.status.previewing')); try { var response = await window.CrankApi.previewOpenApiImport(workspaceId, state.file, { signal: controller.signal }); if (!isCurrent(revision, workspaceId, language)) return; renderPreview(response); setStatus(tKey('openapi.status.preview_ready')); } catch (error) { if (!isCurrent(revision, workspaceId, language)) return; if (error && error.name === 'AbortError') { setStatus(tKey('openapi.status.cancelled')); return; } clearPreview(); showRetry(true); setStatus(errorStatus(error, 'openapi.error.preview'), true, true); } finally { if (isCurrent(revision, workspaceId, language)) { state.previewController = null; qs('openapi-import-preview').disabled = false; qs('openapi-import-cancel').hidden = true; } } } function previewOperationByName(name) { var groups = state.preview && state.preview.groups || []; for (var groupIndex = 0; groupIndex < groups.length; groupIndex += 1) { var operations = groups[groupIndex].operations || []; for (var operationIndex = 0; operationIndex < operations.length; operationIndex += 1) { if (operations[operationIndex].suggested_name === name) return operations[operationIndex]; } } return null; } function wizardHref(operationId) { return ((window.CrankRoutes && window.CrankRoutes.wizard) || '/wizard/') + '?mode=edit&operationId=' + encodeURIComponent(operationId); } function appendFindingNodes(root, findings) { if (!findings || !findings.length) { var empty = document.createElement('span'); empty.className = 'openapi-import-result-ok'; empty.textContent = tKey('openapi.result.no_findings'); root.appendChild(empty); return; } findings.slice(0, 4).forEach(function(finding) { var wrapper = document.createElement('div'); wrapper.innerHTML = renderFinding(finding); root.appendChild(wrapper.firstElementChild); }); if (findings.length > 4) { var more = document.createElement('span'); more.className = 'openapi-import-result-more'; more.textContent = tfKey('openapi.result.more', { count: findings.length - 4 }); root.appendChild(more); } } function renderResult(response) { var node = qs('openapi-import-result'); var title = document.createElement('strong'); node.replaceChildren(); title.textContent = tKey('openapi.result.title'); node.appendChild(title); if (response.created && response.created.length) { var primary = document.createElement('div'); var table = document.createElement('div'); primary.className = 'openapi-import-primary-result'; primary.innerHTML = '' + escapeHtml(tKey('openapi.result.open_first')) + ''; node.appendChild(primary); table.className = 'openapi-import-result-table'; table.setAttribute('role', 'table'); table.innerHTML = '
' + '
' + escapeHtml(tKey('openapi.result.draft')) + '
' + '
' + escapeHtml(tKey('openapi.result.findings')) + '
' + '
' + escapeHtml(tKey('openapi.result.action')) + '
' + '
'; response.created.forEach(function(operation) { var previewOperation = previewOperationByName(operation.name); var row = document.createElement('div'); var nameCell = document.createElement('div'); var name = document.createElement('div'); var meta = document.createElement('div'); var findingsCell = document.createElement('div'); var actionCell = document.createElement('div'); var action = document.createElement('a'); row.className = 'openapi-import-result-row'; row.setAttribute('role', 'row'); nameCell.setAttribute('role', 'cell'); name.className = 'openapi-import-result-name'; name.textContent = operation.name; meta.className = 'openapi-import-result-meta'; meta.textContent = previewOperation ? previewOperation.method + ' ' + previewOperation.path + ' · v' + operation.version : 'v' + operation.version; nameCell.append(name, meta); findingsCell.className = 'openapi-import-result-findings'; findingsCell.setAttribute('role', 'cell'); appendFindingNodes(findingsCell, previewOperation && previewOperation.findings); actionCell.setAttribute('role', 'cell'); action.className = 'btn-secondary openapi-import-result-action'; action.href = wizardHref(operation.operation_id); action.textContent = previewOperation && previewOperation.findings && previewOperation.findings.length ? tKey('openapi.result.fix') : tKey('openapi.result.open'); actionCell.appendChild(action); row.append(nameCell, findingsCell, actionCell); table.appendChild(row); }); node.appendChild(table); } if (response.skipped && response.skipped.length) { var skippedNode = document.createElement('div'); skippedNode.className = 'openapi-import-result-skipped'; skippedNode.textContent = tKey('openapi.result.skipped') + ': ' + response.skipped.map(function(item) { return (item.name || item.operation_key || tKey('openapi.result.unnamed')) + ' — ' + findingMessage({ code: item.code || item.reason_code || item.reason }); }).join('; '); node.appendChild(skippedNode); } if (response.findings && response.findings.length) { var findings = document.createElement('div'); findings.className = 'openapi-import-result-findings'; findings.innerHTML = response.findings.map(renderFinding).join(''); node.appendChild(findings); } node.hidden = false; } async function createDrafts() { if (state.applyInFlight) return; var keys = selectedKeys(); if (!keys.length) { setStatus(tKey('openapi.error.selection'), true, true); return; } if (!state.jobId || !state.preview) { setStatus(tKey('openapi.error.preview_required'), true, true); return; } var serverUrl = (qs('openapi-import-server-custom').value || qs('openapi-import-server').value || '').trim(); if (!serverUrl) { setStatus(tKey('openapi.error.server'), true, true); return; } var revision = state.revision; var workspaceId = state.workspaceId; var language = locale(); var jobId = state.jobId; var controller = new AbortController(); state.applyController = controller; state.applyInFlight = true; qs('openapi-import-create').disabled = true; qs('openapi-import-cancel').hidden = false; showRetry(false); setStatus(tKey('openapi.status.creating')); try { var response = await window.CrankApi.createOpenApiImport(workspaceId, jobId, { selected_operation_keys: keys, server_url: serverUrl, conflict_mode: qs('openapi-import-conflict-mode').value || 'rename', }, { signal: controller.signal }); if (!isCurrent(revision, workspaceId, language)) return; setStatus(tfKey('openapi.status.created', { created: (response.created || []).length, skipped: (response.skipped || []).length, })); renderResult(response); if (typeof state.onImported === 'function') await state.onImported(); } catch (error) { if (!isCurrent(revision, workspaceId, language)) return; if (error && error.name === 'AbortError') { setStatus(tKey('openapi.status.cancelled')); return; } showRetry(false); setStatus(errorStatus(error, 'openapi.error.create'), true, true); } finally { if (isCurrent(revision, workspaceId, language)) { state.applyController = null; state.applyInFlight = false; qs('openapi-import-create').disabled = false; qs('openapi-import-cancel').hidden = true; } } } function reset() { invalidate({ clearFile: true }); showRetry(false); setStatus(''); qs('openapi-import-server-custom').value = ''; qs('openapi-import-conflict-mode').value = 'rename'; } function open(options) { if (state.workspaceId && state.workspaceId !== options.workspaceId) { invalidate({ clearFile: true }); } state.workspaceId = options.workspaceId; state.onImported = options.onImported; state.restoreFocus = document.activeElement; qs('openapi-import-modal').hidden = false; renderFileName(); qs('openapi-import-file-select').focus(); } function close() { invalidate({ clearFile: true }); showRetry(false); qs('openapi-import-modal').hidden = true; if (state.restoreFocus && typeof state.restoreFocus.focus === 'function') { state.restoreFocus.focus(); } } function selectFile(file) { clearServerSelection(); invalidate(); state.file = file || null; showRetry(false); var localError = validateFile(state.file); renderFileName(); if (localError) { setStatus(tKey(localError), true, true); } else if (state.file) { setStatus(tKey('openapi.status.file_ready')); } } document.addEventListener('DOMContentLoaded', function() { if (!qs('openapi-import-modal')) return; renderFileName(); qs('openapi-import-file-select').addEventListener('click', function(event) { event.preventDefault(); qs('openapi-import-file').click(); }); qs('openapi-import-file').addEventListener('change', function(event) { selectFile(event.target.files && event.target.files[0]); }); qs('openapi-import-preview').addEventListener('click', preview); qs('openapi-import-retry').addEventListener('click', preview); qs('openapi-import-create').addEventListener('click', createDrafts); qs('openapi-import-cancel').addEventListener('click', cancelCurrent); qs('openapi-import-reset').addEventListener('click', reset); qs('openapi-import-search').addEventListener('input', function(event) { state.filterQuery = event.target.value.trim(); applyFilters(); }); qs('openapi-import-method-filter').addEventListener('change', function(event) { state.filterMethod = event.target.value; applyFilters(); }); qs('openapi-import-select-visible').addEventListener('click', function() { setVisibleSelection(true); }); qs('openapi-import-clear-visible').addEventListener('click', function() { setVisibleSelection(false); }); document.querySelectorAll('[data-openapi-close]').forEach(function(node) { node.addEventListener('click', close); }); document.addEventListener('keydown', function(event) { var modal = qs('openapi-import-modal'); if (event.key === 'Escape' && !qs('openapi-import-modal').hidden) { event.preventDefault(); close(); } if (event.key !== 'Tab' || modal.hidden) return; var focusable = Array.from(modal.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]), select:not([disabled]), a[href]')) .filter(function(node) { return node.tabIndex >= 0 && !node.closest('[hidden]'); }); if (!focusable.length) return; var first = focusable[0]; var last = focusable[focusable.length - 1]; if (event.shiftKey && (document.activeElement === first || !modal.contains(document.activeElement))) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }); function invalidateForContext(options) { if (!qs('openapi-import-modal').hidden) { invalidate(options); renderFileName(); showRetry(!options || !options.clearFile ? !!state.file : false); setStatus(tKey('openapi.status.context_changed')); } } window.addEventListener('crank:langchange', function() { invalidateForContext(); }); window.addEventListener('crank:workspacechange', function(event) { if (!qs('openapi-import-modal').hidden) { state.workspaceId = event.detail && event.detail.id || null; invalidateForContext({ clearFile: true }); } }); window.addEventListener('storage', function(event) { if (event.key === 'crank_lang') invalidateForContext(); }); window.addEventListener('pagehide', function() { invalidate({ clearFile: true }); showRetry(false); setStatus(''); }); window.addEventListener('pageshow', function(event) { if (!event.persisted || qs('openapi-import-modal').hidden) return; invalidateForContext({ clearFile: true }); }); }); window.CrankOpenApiImport = { open: open, close: close, reset: reset, }; }());