Add destructive confirmation and import guidance

This commit is contained in:
github-ops
2026-06-21 01:42:45 +00:00
parent ef2912855b
commit 5447e1bad0
29 changed files with 1099 additions and 24 deletions
+2
View File
@@ -685,6 +685,7 @@ var TRANSLATIONS = {
'wizard.yaml.none': 'No YAML provided',
'wizard.yaml.none_body': 'Paste a YAML document or load it from a file before importing.',
'wizard.yaml.imported': 'YAML imported',
'wizard.yaml.imported_with_warnings': 'YAML imported with recommendations',
'wizard.yaml.imported_body': 'The imported operation was saved. The wizard will reopen it now.',
'wizard.yaml.loaded': 'YAML loaded',
'wizard.yaml.loaded_body': 'The YAML configuration was loaded.',
@@ -1500,6 +1501,7 @@ var TRANSLATIONS = {
'wizard.yaml.none': 'YAML не предоставлен',
'wizard.yaml.none_body': 'Вставьте YAML-документ или загрузите файл перед импортом.',
'wizard.yaml.imported': 'YAML импортирован',
'wizard.yaml.imported_with_warnings': 'YAML импортирован с рекомендациями',
'wizard.yaml.imported_body': 'Импортированная операция сохранена. Мастер сейчас откроет ее заново.',
'wizard.yaml.loaded': 'YAML загружен',
'wizard.yaml.loaded_body': 'YAML-конфигурация загружена.',
+21 -1
View File
@@ -185,6 +185,22 @@ function showWizardLiveStatus(title, text, isError) {
root.classList.toggle('is-success', !isError);
}
function showPendingImportGuidance() {
var raw = sessionStorage.getItem('crank_import_guidance');
if (!raw) return;
sessionStorage.removeItem('crank_import_guidance');
try {
var warnings = JSON.parse(raw);
if (!Array.isArray(warnings) || warnings.length === 0) return;
showWizardLiveStatus(
tKey('wizard.yaml.imported_with_warnings'),
warnings.map(function(warning) { return '• ' + warning; }).join('\n')
);
} catch (_) {
// Ignore invalid stale session data.
}
}
function currentDraftVersion() {
if (wizardCurrentOperation && wizardCurrentOperation.draft_version_ref) {
return wizardCurrentOperation.draft_version_ref.version;
@@ -487,6 +503,9 @@ async function importWizardYaml() {
return;
}
var imported = await window.CrankApi.importOperation(wizardWorkspaceId, yamlDocument, 'upsert');
if (Array.isArray(imported.warnings) && imported.warnings.length > 0) {
sessionStorage.setItem('crank_import_guidance', JSON.stringify(imported.warnings));
}
showWizardLiveStatus(tKey('wizard.yaml.imported'), tKey('wizard.yaml.imported_body'));
window.location.href = window.location.pathname + '?mode=edit&operationId=' + encodeURIComponent(imported.operation_id);
}
@@ -587,9 +606,10 @@ function copyTestResponseToOutputSample() {
showWizardLiveStatus(tKey('wizard.test.response_copied'), tKey('wizard.test.response_copied_body'));
}
window.CrankWizardLive = {
window.CrankWizardLive = {
saveOperation: saveOperation,
loadOperationForEdit: loadOperationForEdit,
showPendingImportGuidance: showPendingImportGuidance,
bindWizardLiveActions: bindWizardLiveActions,
updateWizardProtocolVisibility: updateWizardProtocolVisibility,
renderAgentFacingPreview: renderAgentFacingPreview,
+3 -1
View File
@@ -17,6 +17,7 @@ var applyEditionProtocolVisibility = window.CrankWizardShell.applyEditionProtoco
var step3PanelId = window.CrankWizardShell.step3PanelId;
var saveOperation = window.CrankWizardLive.saveOperation;
var loadOperationForEdit = window.CrankWizardLive.loadOperationForEdit;
var showPendingImportGuidance = window.CrankWizardLive.showPendingImportGuidance;
var bindWizardLiveActions = window.CrankWizardLive.bindWizardLiveActions;
var updateWizardProtocolVisibility = window.CrankWizardLive.updateWizardProtocolVisibility;
var currentStep = window.currentStep;
@@ -138,6 +139,7 @@ async function initWizardPage() {
window.wizardEditId = wizardEditId;
document.title = 'Crank — ' + tKey('wizard.progress.edit');
await loadOperationForEdit();
showPendingImportGuidance();
}
updateWizardProtocolVisibility();
@@ -172,7 +174,7 @@ if (window.CrankDiagnostics && typeof window.CrankDiagnostics.bootstrap === 'fun
var METHOD_CALLOUTS = {
GET: null,
DELETE: null,
DELETE: { icon: 'info', text: { en: { title: 'DELETE selected — confirmation is required.', body: 'Crank will expose this MCP tool as a two-step action: the first call returns a confirmation token, the second call with the same arguments and token executes the API request.' }, ru: { title: 'Выбран DELETE — требуется подтверждение.', body: 'Crank откроет этот MCP инструмент как двухшаговое действие: первый вызов вернет токен подтверждения, второй вызов с теми же аргументами и токеном выполнит запрос к API.' } } },
POST: { icon: 'info', text: { en: { title: 'POST selected — request body is required.', body: 'In step 4 you will define the JSON schema for the request body. Crank will convert MCP tool parameters into the selected format.' }, ru: { title: 'Выбран POST — требуется тело запроса.', body: 'На шаге 4 вы зададите JSON-схему тела запроса. Crank преобразует параметры MCP инструмента в выбранный формат.' } } },
PUT: { icon: 'info', text: { en: { title: 'PUT selected — full-resource replacement.', body: 'The request body must contain a complete resource representation. Define the request schema in step 4.' }, ru: { title: 'Выбран PUT — полная замена ресурса.', body: 'Тело запроса должно содержать полное представление ресурса. Задайте схему запроса на шаге 4.' } } },
PATCH: { icon: 'info', text: { en: { title: 'PATCH selected — partial update.', body: 'Only include the fields you want to modify in the request schema defined in step 4.' }, ru: { title: 'Выбран PATCH — частичное обновление.', body: 'В схему запроса на шаге 4 включайте только те поля, которые хотите изменить.' } } },
+3 -2
View File
@@ -85,8 +85,9 @@ test('REST method callout clears when switching to methods without request body'
await expect(callout).toContainText(/PATCH|Выбран PATCH/);
await page.locator('.method-card[data-method="DELETE"]').click();
await expect(callout).toBeHidden();
await expect(callout).toHaveText('');
await expect(callout).toBeVisible();
await expect(callout).toContainText(/DELETE|Выбран DELETE/);
await expect(callout).toContainText(/confirmation|подтверждение/i);
await page.locator('.method-card[data-method="GET"]').click();
await expect(callout).toBeHidden();