Files
crank/apps/ui/js/openapi-import.js
T
bsodfather bc03c33387
CI / Rust Checks (push) Failing after 4m6s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped
fix(openapi): harden story 2.1 production lifecycle
2026-08-29 00:48:22 +03:00

1140 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function() {
var MAX_FILE_BYTES = 256 * 1024;
var APPLY_REPLAY_STORAGE_KEY = 'crank_openapi_apply_replay_v1';
var restoredApplyRequest = loadApplyReplay();
var state = {
workspaceId: null,
onImported: null,
file: null,
jobId: null,
preview: null,
filterQuery: '',
filterMethod: '',
revision: 0,
previewController: null,
applyController: null,
applyInFlight: false,
applyRequest: restoredApplyRequest,
applyMetadata: restoredApplyRequest && restoredApplyRequest.operationMetadata || [],
applyResult: null,
applyOutcomeUnknown: !!restoredApplyRequest,
applyRefreshFailed: false,
restoreFocus: null,
};
function validApplyReplay(request) {
return request
&& typeof request.workspaceId === 'string' && request.workspaceId.length > 0 && request.workspaceId.length <= 128
&& typeof request.jobId === 'string' && request.jobId.length > 0 && request.jobId.length <= 128
&& request.payload && Array.isArray(request.payload.selected_operation_keys)
&& request.payload.selected_operation_keys.length <= 1024
&& request.payload.selected_operation_keys.every(function(key) {
return typeof key === 'string' && key.length > 0 && key.length <= 512;
})
&& (request.payload.server_url === null
|| (typeof request.payload.server_url === 'string' && request.payload.server_url.length <= 2048))
&& (request.payload.conflict_mode === 'skip' || request.payload.conflict_mode === 'rename')
&& Array.isArray(request.operationMetadata)
&& request.operationMetadata.length <= 1024
&& request.operationMetadata.every(function(operation) {
return operation
&& typeof operation.key === 'string' && operation.key.length > 0 && operation.key.length <= 512
&& typeof operation.method === 'string' && operation.method.length > 0 && operation.method.length <= 16
&& typeof operation.path === 'string' && operation.path.length > 0 && operation.path.length <= 2048;
});
}
function loadApplyReplay() {
try {
var request = JSON.parse(sessionStorage.getItem(APPLY_REPLAY_STORAGE_KEY) || 'null');
return validApplyReplay(request) ? request : null;
} catch (_error) {
return null;
}
}
function persistApplyReplay(request) {
if (!validApplyReplay(request)) return;
try {
sessionStorage.setItem(APPLY_REPLAY_STORAGE_KEY, JSON.stringify(request));
} catch (_error) {}
}
function clearApplyReplay() {
try {
sessionStorage.removeItem(APPLY_REPLAY_STORAGE_KEY);
} catch (_error) {}
}
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 isSameContext(revision, workspaceId, language) {
return revision === state.revision
&& workspaceId === state.workspaceId
&& language === locale();
}
function isCurrent(revision, workspaceId, language) {
return isSameContext(revision, workspaceId, language)
&& !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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function renderFinding(finding) {
var severity = String(finding && finding.severity || 'warning').toLowerCase();
var label = severity === 'info' ? 'i' : severity === 'error' ? '×' : '!';
return '<span class="openapi-import-finding openapi-import-finding-' + escapeHtml(severity) + '">'
+ '<strong>' + label + '</strong> '
+ escapeHtml(findingMessage(finding))
+ '</span>';
}
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.required');
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 = '<span>' + escapeHtml(group.title) + ' · ' + group.operations.length + '</span>'
+ '<span class="openapi-import-group-count" data-openapi-group-count></span>'
+ '<label class="openapi-import-group-toggle"><input type="checkbox" data-openapi-group checked> '
+ escapeHtml(tKey('openapi.group.select'))
+ '</label>';
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 = '<input type="checkbox" data-openapi-operation value="' + escapeHtml(operation.key) + '" checked>'
+ '<div><div class="openapi-import-operation-title">' + escapeHtml(operation.suggested_display_name) + '</div>'
+ '<div class="openapi-import-operation-meta"><code>' + escapeHtml(operation.suggested_name) + '</code> · '
+ escapeHtml(operation.path) + ' · '
+ escapeHtml(tfKey('openapi.operation.fields', { input: operation.input_fields, output: operation.output_fields }))
+ '</div></div><span class="openapi-import-method">' + escapeHtml(operation.method) + '</span>';
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.applyRequest = null;
state.applyMetadata = [];
state.applyResult = null;
state.applyOutcomeUnknown = false;
state.applyRefreshFailed = false;
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-file-select').disabled = false;
qs('openapi-import-reset').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 lockApplyInputs(locked) {
qs('openapi-import-create').disabled = locked;
qs('openapi-import-preview').disabled = locked;
qs('openapi-import-file-select').disabled = locked;
qs('openapi-import-reset').disabled = locked;
}
function markApplyOutcomeUnknown(options) {
if (!state.applyRequest) return false;
abort(state.applyController);
state.applyController = null;
state.applyInFlight = false;
state.applyOutcomeUnknown = true;
persistApplyReplay(state.applyRequest);
// Until the same job is replayed, starting another import could hide an
// already committed server outcome and lead to duplicate drafts.
lockApplyInputs(true);
qs('openapi-import-cancel').hidden = true;
if (!options || options.render !== false) {
showRetry(true);
setStatus(tKey('openapi.status.apply_outcome_unknown'), true, true);
}
return true;
}
function cancelCurrent() {
if (state.applyInFlight || state.applyController) {
markApplyOutcomeUnknown();
return;
}
if (!state.previewController) 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 previewOperationForCreated(created) {
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 (created.operation_key && operations[operationIndex].key === created.operation_key) {
return operations[operationIndex];
}
}
}
// Older servers did not return operation_key. Keep result rendering useful
// during a rolling upgrade, but use the unambiguous key whenever it exists.
for (var fallbackGroupIndex = 0; fallbackGroupIndex < groups.length; fallbackGroupIndex += 1) {
var fallbackOperations = groups[fallbackGroupIndex].operations || [];
for (var fallbackOperationIndex = 0; fallbackOperationIndex < fallbackOperations.length; fallbackOperationIndex += 1) {
if (fallbackOperations[fallbackOperationIndex].suggested_name === created.name) {
return fallbackOperations[fallbackOperationIndex];
}
}
}
for (var metadataIndex = 0; metadataIndex < state.applyMetadata.length; metadataIndex += 1) {
if (created.operation_key && state.applyMetadata[metadataIndex].key === created.operation_key) {
return state.applyMetadata[metadataIndex];
}
}
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 = '<a class="btn-primary" href="' + wizardHref(response.created[0].operation_id) + '">'
+ escapeHtml(tKey('openapi.result.open_first'))
+ '</a>';
node.appendChild(primary);
table.className = 'openapi-import-result-table';
table.setAttribute('role', 'table');
table.innerHTML = '<div class="openapi-import-result-row openapi-import-result-head" role="row">'
+ '<div role="columnheader">' + escapeHtml(tKey('openapi.result.draft')) + '</div>'
+ '<div role="columnheader">' + escapeHtml(tKey('openapi.result.findings')) + '</div>'
+ '<div role="columnheader">' + escapeHtml(tKey('openapi.result.action')) + '</div>'
+ '</div>';
response.created.forEach(function(operation) {
var previewOperation = previewOperationForCreated(operation);
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;
}
function createdStatus(response) {
return tfKey('openapi.status.created', {
created: (response.created || []).length,
skipped: (response.skipped || []).length,
});
}
async function notifyImported(revision, workspaceId, language) {
if (typeof state.onImported !== 'function') return;
try {
await state.onImported();
} catch (_error) {
state.applyRefreshFailed = true;
if (isCurrent(revision, workspaceId, language) && state.applyResult) {
setStatus(createdStatus(state.applyResult) + ' ' + tKey('openapi.status.refresh_failed'), false, true);
}
}
}
async function submitApply(request) {
if (state.applyInFlight) return;
var revision = state.revision;
var workspaceId = request.workspaceId;
var language = locale();
var controller = new AbortController();
state.applyController = controller;
state.applyInFlight = true;
state.applyOutcomeUnknown = false;
state.applyRefreshFailed = false;
lockApplyInputs(true);
qs('openapi-import-cancel').hidden = false;
showRetry(false);
setStatus(tKey('openapi.status.creating'));
try {
var response = await window.CrankApi.createOpenApiImport(
workspaceId,
request.jobId,
request.payload,
{ signal: controller.signal }
);
// Abort is advisory: a superseded request can still deliver a response.
// Only the controller installed by this attempt may mutate UI authority.
if (state.applyController !== controller) return;
state.applyOutcomeUnknown = false;
clearApplyReplay();
state.applyController = null;
state.applyInFlight = false;
lockApplyInputs(false);
qs('openapi-import-cancel').hidden = true;
showRetry(false);
if (!isSameContext(revision, workspaceId, language)) {
// The original workspace job is now resolved. Do not render its result
// into another workspace/language context.
state.applyResult = null;
state.applyRequest = null;
return;
}
state.applyResult = response;
state.applyRequest = null;
if (isCurrent(revision, workspaceId, language)) {
setStatus(createdStatus(response));
renderResult(response);
}
await notifyImported(revision, workspaceId, language);
} catch (error) {
// A cancelled attempt may settle after an idempotent retry has already
// installed a new controller. It must not overwrite the newer state.
if (state.applyController !== controller) return;
if (error && error.name === 'AbortError') {
state.applyOutcomeUnknown = true;
persistApplyReplay(state.applyRequest);
showRetry(true);
if (!qs('openapi-import-modal').hidden) {
setStatus(tKey('openapi.status.apply_outcome_unknown'), true, true);
}
return;
}
if (Number.isInteger(error && error.status)
&& error.status >= 400 && error.status < 500 && error.status !== 408) {
state.applyOutcomeUnknown = false;
state.applyRequest = null;
clearApplyReplay();
showRetry(true);
if (!qs('openapi-import-modal').hidden) {
setStatus(errorStatus(error, 'openapi.error.create'), true, true);
}
return;
}
// A browser/network failure is not proof that the server rolled the
// transaction back. Replaying this exact job is idempotent server-side.
state.applyOutcomeUnknown = true;
persistApplyReplay(state.applyRequest);
showRetry(true);
if (!qs('openapi-import-modal').hidden) {
setStatus(errorStatus(error, 'openapi.error.create') + ' ' + tKey('openapi.status.apply_outcome_unknown'), true, true);
}
} finally {
if (state.applyController === controller) {
state.applyController = null;
state.applyInFlight = false;
lockApplyInputs(state.applyOutcomeUnknown);
qs('openapi-import-cancel').hidden = true;
}
}
}
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;
}
state.applyRequest = {
workspaceId: state.workspaceId,
jobId: state.jobId,
payload: {
selected_operation_keys: keys,
server_url: serverUrl,
conflict_mode: qs('openapi-import-conflict-mode').value || 'rename',
},
operationMetadata: (state.preview.groups || []).flatMap(function(group) {
return (group.operations || []).filter(function(operation) {
return keys.indexOf(operation.key) >= 0;
}).map(function(operation) {
return { key: operation.key, method: operation.method, path: operation.path };
});
}),
};
state.applyMetadata = state.applyRequest.operationMetadata;
persistApplyReplay(state.applyRequest);
await submitApply(state.applyRequest);
}
function retryCurrent() {
if (state.applyOutcomeUnknown && state.applyRequest) {
return submitApply(state.applyRequest);
}
return preview();
}
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
&& !(state.applyOutcomeUnknown && state.applyRequest)) {
invalidate({ clearFile: true });
}
state.workspaceId = options.workspaceId;
state.onImported = options.onImported;
state.restoreFocus = document.activeElement;
qs('openapi-import-modal').hidden = false;
renderFileName();
if (state.applyResult) {
renderResult(state.applyResult);
setStatus(createdStatus(state.applyResult) + (state.applyRefreshFailed ? ' ' + tKey('openapi.status.refresh_failed') : ''));
} else if (state.applyOutcomeUnknown && state.applyRequest) {
lockApplyInputs(true);
showRetry(true);
setStatus(tKey('openapi.status.apply_outcome_unknown'), true, true);
qs('openapi-import-retry').focus();
return;
}
qs('openapi-import-file-select').focus();
}
function close() {
if (state.applyInFlight || state.applyController) {
markApplyOutcomeUnknown({ render: false });
qs('openapi-import-modal').hidden = true;
if (state.restoreFocus && typeof state.restoreFocus.focus === 'function') {
state.restoreFocus.focus();
}
return;
}
if (state.applyOutcomeUnknown && state.applyRequest) {
persistApplyReplay(state.applyRequest);
qs('openapi-import-modal').hidden = true;
if (state.restoreFocus && typeof state.restoreFocus.focus === 'function') {
state.restoreFocus.focus();
}
return;
}
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', retryCurrent);
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) {
if (state.applyInFlight || state.applyController) {
markApplyOutcomeUnknown();
return;
}
if (state.applyOutcomeUnknown && state.applyRequest) {
showRetry(true);
setStatus(tKey('openapi.status.apply_outcome_unknown'), true, true);
return;
}
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() {
if (state.applyInFlight || state.applyController) {
markApplyOutcomeUnknown({ render: false });
state.file = null;
qs('openapi-import-file').value = '';
renderFileName();
} else if (state.applyOutcomeUnknown && state.applyRequest) {
persistApplyReplay(state.applyRequest);
} else {
invalidate({ clearFile: true });
}
clearServerSelection();
showRetry(false);
setStatus('');
});
window.addEventListener('pageshow', function(event) {
if (!event.persisted || qs('openapi-import-modal').hidden) return;
if (state.applyOutcomeUnknown && state.applyRequest) {
lockApplyInputs(true);
showRetry(true);
setStatus(tKey('openapi.status.apply_outcome_unknown'), true, true);
} else {
invalidateForContext({ clearFile: true });
}
});
});
window.CrankOpenApiImport = {
open: open,
close: close,
reset: reset,
};
}());