1140 lines
40 KiB
JavaScript
1140 lines
40 KiB
JavaScript
(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, '&')
|
||
.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 '<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,
|
||
};
|
||
}());
|