diff --git a/Cargo.lock b/Cargo.lock index dc33500..e86277a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,7 +2065,7 @@ dependencies = [ [[package]] name = "secrets-mcp" -version = "0.5.17" +version = "0.5.18" dependencies = [ "anyhow", "askama", diff --git a/crates/secrets-mcp/Cargo.toml b/crates/secrets-mcp/Cargo.toml index 9ff111f..fd62cb7 100644 --- a/crates/secrets-mcp/Cargo.toml +++ b/crates/secrets-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "secrets-mcp" -version = "0.5.17" +version = "0.5.18" edition.workspace = true [[bin]] diff --git a/crates/secrets-mcp/src/web/entries.rs b/crates/secrets-mcp/src/web/entries.rs index 652b3b0..ec5c247 100644 --- a/crates/secrets-mcp/src/web/entries.rs +++ b/crates/secrets-mcp/src/web/entries.rs @@ -138,6 +138,25 @@ pub(super) struct EntryOptionQuery { type EntryApiError = (StatusCode, Json); +fn require_encryption_key(headers: &HeaderMap, lang: UiLang) -> Result<[u8; 32], EntryApiError> { + let enc_key_hex = headers + .get("x-encryption-key") + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + ( + StatusCode::BAD_REQUEST, + Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })), + ) + })?; + + secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })), + ) + }) +} + fn map_entry_mutation_err(e: anyhow::Error, lang: UiLang) -> EntryApiError { if let Some(app_err) = e.downcast_ref::() { return map_app_error(app_err, lang); @@ -876,6 +895,7 @@ pub(super) struct SecretPatchBody { name: Option, #[serde(rename = "type")] secret_type: Option, + value: Option, } pub(super) async fn api_secret_patch( @@ -901,6 +921,7 @@ pub(super) async fn api_secret_patch( let name = body.name.as_ref().map(|s| s.trim()); let secret_type = body.secret_type.as_ref().map(|s| s.trim()); + let secret_value = body.value.as_ref(); if let Some(n) = name { if n.is_empty() { @@ -940,30 +961,37 @@ pub(super) async fn api_secret_patch( } } - if name.is_none() && secret_type.is_none() { + if name.is_none() && secret_type.is_none() && secret_value.is_none() { return Err(( StatusCode::BAD_REQUEST, Json( - json!({ "error": tr(lang, "至少需要提供 name 或 type 之一", "至少需要提供 name 或 type 之一", "At least one of name or type is required") }), + json!({ "error": tr(lang, "至少需要提供 name、type 或 value 之一", "至少需要提供 name、type 或 value 之一", "At least one of name, type, or value is required") }), ), )); } + let master_key = if secret_value.is_some() { + Some(require_encryption_key(&headers, lang)?) + } else { + None + }; + let mut tx = state .pool .begin() .await .map_err(|e| map_entry_mutation_err(e.into(), lang))?; - let secret_row: Option<(String, String)> = - sqlx::query_as("SELECT name, type FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE") - .bind(secret_id) - .bind(user_id) - .fetch_optional(&mut *tx) - .await - .map_err(|e| map_entry_mutation_err(e.into(), lang))?; + let secret_row: Option<(String, String, Vec)> = sqlx::query_as( + "SELECT name, type, encrypted FROM secrets WHERE id = $1 AND user_id = $2 FOR UPDATE", + ) + .bind(secret_id) + .bind(user_id) + .fetch_optional(&mut *tx) + .await + .map_err(|e| map_entry_mutation_err(e.into(), lang))?; - let Some((old_name, old_type)) = secret_row else { + let Some((old_name, old_type, old_encrypted)) = secret_row else { let _ = tx.rollback().await; return Err(( StatusCode::NOT_FOUND, @@ -988,13 +1016,47 @@ pub(super) async fn api_secret_patch( let new_name = name.unwrap_or(&old_name).to_string(); let new_type = secret_type.unwrap_or(&old_type).to_string(); + let new_encrypted = if let Some(value) = secret_value { + let encrypted = secrets_core::crypto::encrypt_json( + master_key + .as_ref() + .ok_or_else(|| { + ( + StatusCode::BAD_REQUEST, + Json(json!({ "error": tr(lang, "请先设置密码短语后再编辑密文值", "請先設定密碼短語後再編輯密文值", "Unlock your passphrase before editing secret values") })), + ) + })?, + value, + ) + .map_err(|e| map_entry_mutation_err(e, lang))?; + Some(encrypted) + } else { + None + }; + + let value_changed = new_encrypted.is_some(); + + if let Err(e) = secrets_core::db::snapshot_secret_history( + &mut tx, + secrets_core::db::SecretSnapshotParams { + secret_id, + name: &old_name, + encrypted: &old_encrypted, + action: if value_changed { "update" } else { "rename" }, + }, + ) + .await + { + tracing::warn!(error = %e, %secret_id, "failed to snapshot secret history before patch"); + } let result = sqlx::query( - "UPDATE secrets SET name = $1, type = $2, version = version + 1, updated_at = NOW() \ - WHERE id = $3", + "UPDATE secrets SET name = $1, type = $2, encrypted = $3, version = version + 1, updated_at = NOW() \ + WHERE id = $4", ) .bind(&new_name) .bind(&new_type) + .bind(new_encrypted.as_deref().unwrap_or(&old_encrypted)) .bind(secret_id) .execute(&mut *tx) .await; @@ -1018,7 +1080,11 @@ pub(super) async fn api_secret_patch( secrets_core::audit::log_tx( &mut tx, Some(user_id), - "rename_secret", + if value_changed { + "update_secret" + } else { + "rename_secret" + }, "", "", &old_name, @@ -1029,6 +1095,7 @@ pub(super) async fn api_secret_patch( "new_name": new_name, "old_type": old_type, "new_type": new_type, + "value_updated": value_changed, "linked_entries": linked_entries, }), ) @@ -1154,23 +1221,7 @@ pub(super) async fn api_entry_secrets_decrypt( Json(json!({ "error": tr(lang, "未登录", "尚未登入", "Not logged in") })), ))?; - let enc_key_hex = headers - .get("x-encryption-key") - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - ( - StatusCode::BAD_REQUEST, - Json(json!({ "error": tr(lang, "缺少 X-Encryption-Key 请求头", "缺少 X-Encryption-Key 請求標頭", "Missing X-Encryption-Key header") })), - ) - })?; - - let master_key = - secrets_core::crypto::extract_key_from_hex(enc_key_hex).map_err(|_| { - ( - StatusCode::BAD_REQUEST, - Json(json!({ "error": tr(lang, "X-Encryption-Key 格式无效", "X-Encryption-Key 格式無效", "Invalid X-Encryption-Key format") })), - ) - })?; + let master_key = require_encryption_key(&headers, lang)?; let secrets = get_all_secrets_by_id(&state.pool, entry_id, &master_key, Some(user_id)) diff --git a/crates/secrets-mcp/templates/entries.html b/crates/secrets-mcp/templates/entries.html index ea066f5..aaa47a7 100644 --- a/crates/secrets-mcp/templates/entries.html +++ b/crates/secrets-mcp/templates/entries.html @@ -456,7 +456,16 @@ padding: 7px 10px; word-break: break-all; white-space: pre-wrap; max-height: 140px; overflow: auto; color: #c9d1d9; line-height: 1.5; } - + .view-secret-editor { + width: 100%; min-height: 108px; resize: vertical; box-sizing: border-box; + font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.5; + background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px; + color: #c9d1d9; padding: 10px 12px; outline: none; + } + .view-secret-editor:focus { border-color: rgba(56,139,253,0.5); } + .view-secret-hint { + margin-top: 6px; font-size: 12px; color: #8b949e; line-height: 1.5; + } .btn-icon { padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer; border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e; @@ -484,7 +493,7 @@ .btn-view-edit { color: #58a6ff; } .btn-view-save { color: #3fb950; } .btn-view-cancel { color: #8b949e; } - .btn-view-unlink { color: #f85149; font-size: 14px; } + .btn-view-unlink { color: #f85149; font-size: 12px; white-space: nowrap; } @@ -763,6 +772,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option viewCopy: '复制', viewCopied: '已复制', viewLoading: '解密中…', + viewEditSecret: '编辑密文', + viewValueHintJson: '此值按 JSON 保存,请输入合法 JSON。', + viewValueInvalidJson: '请输入合法 JSON 值', + viewValueUnlockRequired: '请先在 MCP 配置页解锁密码短语后再修改密文值。', viewSaveChanges: '保存更改', viewChangesSaved: '已保存', viewUnlinkConfirm: '确定解除密文关联「{name}」?', @@ -843,6 +856,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option viewCopy: '複製', viewCopied: '已複製', viewLoading: '解密中…', + viewEditSecret: '編輯密文', + viewValueHintJson: '此值會以 JSON 儲存,請輸入合法 JSON。', + viewValueInvalidJson: '請輸入合法 JSON 值', + viewValueUnlockRequired: '請先在 MCP 設定頁解鎖密碼短語,再修改密文值。', viewSaveChanges: '儲存變更', viewChangesSaved: '已儲存', viewUnlinkConfirm: '確定解除密文關聯「{name}」?', @@ -923,6 +940,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option viewCopy: 'Copy', viewCopied: 'Copied', viewLoading: 'Decrypting…', + viewEditSecret: 'Edit secret', + viewValueHintJson: 'This value is stored as JSON. Enter valid JSON.', + viewValueInvalidJson: 'Enter a valid JSON value', + viewValueUnlockRequired: 'Unlock your passphrase on the MCP config page before editing secret values.', viewSaveChanges: 'Save changes', viewChangesSaved: 'Saved', viewUnlinkConfirm: 'Unlink secret "{name}"?', @@ -1161,9 +1182,81 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option if (e.target === viewOverlay) closeView(); }); + function parseEntrySecretSchema(tr) { + if (!tr) return []; + try { + var raw = JSON.parse(tr.getAttribute('data-entry-secrets') || '[]'); + return Array.isArray(raw) ? raw : []; + } catch (err) { + return []; + } + } + + function renderEntrySecretChips(tr, secretSchema) { + if (!tr) return; + var list = tr.querySelector('.col-secrets .secret-list'); + if (!list) return; + list.innerHTML = ''; + (secretSchema || []).forEach(function (secret) { + var chip = document.createElement('span'); + chip.className = 'secret-chip'; + chip.setAttribute('data-secret-id', secret.id || ''); + + var name = document.createElement('span'); + name.className = 'secret-name'; + name.title = secret.name || ''; + name.textContent = secret.name || ''; + + var type = document.createElement('span'); + type.className = 'secret-type'; + type.textContent = secret.secret_type || 'text'; + + chip.appendChild(name); + chip.appendChild(type); + list.appendChild(chip); + }); + + var viewBtn = tr.querySelector('.btn-view-secrets'); + if (viewBtn) viewBtn.disabled = !(secretSchema && secretSchema.length); + } + + function writeEntrySecretSchema(entryId, secretSchema) { + var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]'); + if (!tr) return; + tr.setAttribute('data-entry-secrets', JSON.stringify(secretSchema || [])); + renderEntrySecretChips(tr, secretSchema || []); + } + + function updateEntrySecretSchema(entryId, secretId, updater) { + var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]'); + if (!tr) return; + var changed = false; + var next = parseEntrySecretSchema(tr).map(function (secret) { + if (String(secret.id || '') !== String(secretId || '')) return secret; + changed = true; + return updater(Object.assign({}, secret)); + }); + if (changed) writeEntrySecretSchema(entryId, next); + } + + function removeEntrySecretSchema(entryId, secretId) { + var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]'); + if (!tr) return; + var schema = parseEntrySecretSchema(tr); + writeEntrySecretSchema(entryId, schema.filter(function (secret) { + return String(secret.id || '') !== String(secretId || ''); + })); + } + function renderViewSecrets(secrets, secretSchema) { viewBody.innerHTML = ''; - var names = Object.keys(secrets); + var names = []; + (secretSchema || []).forEach(function (secret) { + if (Object.prototype.hasOwnProperty.call(secrets, secret.name)) names.push(secret.name); + }); + Object.keys(secrets).forEach(function (name) { + if (names.indexOf(name) === -1) names.push(name); + }); if (names.length === 0) { var msg = document.createElement('div'); msg.className = 'view-locked-msg'; @@ -1177,18 +1270,42 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option names.forEach(function (name) { var raw = secrets[name]; - var valueStr = (raw === null || raw === undefined) ? '' : - (typeof raw === 'object') ? JSON.stringify(raw, null, 2) : String(raw); + var currentName = name; + var valueMode = (typeof raw === 'string') ? 'text' : 'json'; + var valueStr = (typeof raw === 'string') ? raw : JSON.stringify(raw, null, 2); + var schema = schemaMap[name] || {}; var secretId = schema.id || ''; - var secretType = schema.secret_type || 'text'; - var originalName = name; - var hasChanges = false; + var currentType = schema.secret_type || 'text'; + + function formatSecretValue(value) { + return (typeof value === 'string') ? value : JSON.stringify(value, null, 2); + } + + function parseEditedSecretValue(text) { + if (valueMode === 'text') return { ok: true, value: text }; + try { + return { ok: true, value: JSON.parse(text) }; + } catch (err) { + return { ok: false, error: t('viewValueInvalidJson') }; + } + } + + function comparableSecretValue(value) { + return JSON.stringify(value); + } + + function applyCurrentSecretValue(value) { + raw = value; + valueStr = formatSecretValue(value); + valueEl.textContent = valueStr; + valueEditor.value = valueStr; + } var row = document.createElement('div'); row.className = 'view-secret-row'; row.setAttribute('data-secret-id', secretId); - row.setAttribute('data-original-name', originalName); + row.setAttribute('data-original-name', currentName); var header = document.createElement('div'); header.className = 'view-secret-header'; @@ -1203,14 +1320,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option var nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'view-secret-name-input'; - nameInput.value = name; + nameInput.value = currentName; nameInput.placeholder = t('renameSecretPlaceholder'); - nameInput.setAttribute('data-original-name', originalName); nameInput.hidden = true; var typeBadge = document.createElement('span'); typeBadge.className = 'view-secret-type'; - typeBadge.textContent = secretType; + typeBadge.textContent = currentType; var typeSelect = document.createElement('select'); typeSelect.className = 'view-secret-type-select'; @@ -1219,13 +1335,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option var option = document.createElement('option'); option.value = opt; option.textContent = opt; - if (opt === secretType) option.selected = true; + if (opt === currentType) option.selected = true; typeSelect.appendChild(option); }); - if (SECRET_TYPE_OPTIONS.indexOf(secretType) === -1 && secretType) { + if (SECRET_TYPE_OPTIONS.indexOf(currentType) === -1 && currentType) { var fallback = document.createElement('option'); - fallback.value = secretType; - fallback.textContent = secretType; + fallback.value = currentType; + fallback.textContent = currentType; fallback.selected = true; typeSelect.appendChild(fallback); } @@ -1242,8 +1358,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option var editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'btn-icon btn-view-edit'; - editBtn.textContent = '✎'; - editBtn.title = t('renameSecretTitle'); + editBtn.textContent = t('viewEditSecret'); var saveBtn = document.createElement('button'); saveBtn.type = 'button'; @@ -1272,8 +1387,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option var unlinkBtn = document.createElement('button'); unlinkBtn.type = 'button'; unlinkBtn.className = 'btn-icon btn-view-unlink'; - unlinkBtn.textContent = '×'; - unlinkBtn.title = t('unlinkTitle'); + unlinkBtn.textContent = t('unlinkTitle'); actions.appendChild(unlinkBtn); actions.appendChild(editBtn); @@ -1287,9 +1401,23 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option var valueEl = document.createElement('div'); valueEl.className = 'view-secret-value'; valueEl.textContent = valueStr; + var valueEditor = document.createElement('textarea'); + valueEditor.className = 'view-secret-editor'; + valueEditor.hidden = true; + valueEditor.value = valueStr; valueWrap.appendChild(valueEl); + valueWrap.appendChild(valueEditor); row.appendChild(valueWrap); + var valueHint = null; + if (valueMode === 'json') { + valueHint = document.createElement('div'); + valueHint.className = 'view-secret-hint'; + valueHint.hidden = true; + valueHint.textContent = t('viewValueHintJson'); + row.appendChild(valueHint); + } + var nameStatus = document.createElement('div'); nameStatus.className = 'secret-name-status'; nameStatus.setAttribute('data-status', 'idle'); @@ -1301,8 +1429,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option function enterEditMode() { nameSpan.hidden = true; typeBadge.hidden = true; + valueEl.hidden = true; nameInput.hidden = false; typeSelect.hidden = false; + valueEditor.hidden = false; + if (valueHint) valueHint.hidden = false; saveBtn.hidden = false; cancelBtn.hidden = false; editBtn.hidden = true; @@ -1313,8 +1444,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option function exitEditMode() { nameSpan.hidden = false; typeBadge.hidden = false; + valueEl.hidden = false; nameInput.hidden = true; typeSelect.hidden = true; + valueEditor.hidden = true; + if (valueHint) valueHint.hidden = true; saveBtn.hidden = true; cancelBtn.hidden = true; editBtn.hidden = false; @@ -1322,7 +1456,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option nameStatus.className = 'secret-name-status'; nameInput.value = nameSpan.textContent; typeSelect.value = typeBadge.textContent; - hasChanges = false; + valueEditor.value = valueStr; } editBtn.addEventListener('click', enterEditMode); @@ -1337,7 +1471,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option nameStatus.className = 'secret-name-status'; debounceTimer = setTimeout(function () { var newName = nameInput.value.trim(); - if (!newName || newName === originalName) return; + if (!newName || newName === currentName) return; nameStatus.textContent = t('checkingSecretName'); nameStatus.className = 'secret-name-status checking'; var checkId = Date.now(); @@ -1352,18 +1486,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option if (data.ok && data.available) { nameStatus.textContent = t('secretNameAvailable'); nameStatus.className = 'secret-name-status success'; - hasChanges = true; } else { nameStatus.textContent = data.error || t('secretNameTaken'); nameStatus.className = 'secret-name-status error'; - hasChanges = false; } }) .catch(function () { if (currentCheck !== checkId) return; nameStatus.textContent = t('secretNameCheckError'); nameStatus.className = 'secret-name-status error'; - hasChanges = false; }); }, 300); }); @@ -1372,21 +1503,46 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option if (e.key === 'Enter') { e.preventDefault(); saveBtn.click(); } if (e.key === 'Escape') { cancelBtn.click(); } }); + valueEditor.addEventListener('keydown', function (e) { + if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { + e.preventDefault(); + saveBtn.click(); + } + if (e.key === 'Escape') cancelBtn.click(); + }); // ── Save ── saveBtn.addEventListener('click', function () { var newName = nameInput.value.trim(); var newType = typeSelect.value; + var parsedValue = parseEditedSecretValue(valueEditor.value); if (!newName) { nameStatus.textContent = t('secretNameInvalid'); nameStatus.className = 'secret-name-status error'; return; } if (!newType) { nameStatus.textContent = t('secretTypeInvalid'); nameStatus.className = 'secret-name-status error'; return; } + if (!parsedValue.ok) { nameStatus.textContent = parsedValue.error; nameStatus.className = 'secret-name-status error'; return; } + + var nextValue = parsedValue.value; var patchBody = {}; - if (newName !== originalName) patchBody.name = newName; - if (newType !== secretType) patchBody.type = newType; + var valueChanged = comparableSecretValue(nextValue) !== comparableSecretValue(raw); + if (newName !== currentName) patchBody.name = newName; + if (newType !== currentType) patchBody.type = newType; + if (valueChanged) patchBody.value = nextValue; if (Object.keys(patchBody).length === 0) { exitEditMode(); return; } + var encKey = sessionStorage.getItem('enc_key'); + if (valueChanged && !encKey) { + nameStatus.textContent = t('viewValueUnlockRequired'); + nameStatus.className = 'secret-name-status error'; + return; + } saveBtn.textContent = '...'; + saveBtn.disabled = true; + cancelBtn.disabled = true; + editBtn.disabled = true; fetch('/api/secrets/' + encodeURIComponent(secretId), { method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, + headers: Object.assign( + { 'Content-Type': 'application/json' }, + valueChanged ? { 'X-Encryption-Key': encKey } : {} + ), credentials: 'same-origin', body: JSON.stringify(patchBody) }).then(function (r) { @@ -1395,22 +1551,28 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option return data; }); }).then(function () { + currentName = newName; + currentType = newType; nameSpan.textContent = newName; typeBadge.textContent = newType; - originalName = newName; - nameInput.setAttribute('data-original-name', newName); + applyCurrentSecretValue(nextValue); saveBtn.textContent = t('viewChangesSaved'); + nameStatus.textContent = t('viewChangesSaved'); + nameStatus.className = 'secret-name-status success'; + updateEntrySecretSchema(viewBody.getAttribute('data-entry-id'), secretId, function (secret) { + secret.name = newName; + secret.secret_type = newType; + return secret; + }); setTimeout(function () { exitEditMode(); saveBtn.textContent = t('viewSaveChanges'); }, 1200); - // Update table row chip - var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]'); - if (tableRow) { - var chip = tableRow.querySelector('.secret-chip .secret-name'); - if (chip && chip.textContent === name) chip.textContent = newName; - } }).catch(function (err) { nameStatus.textContent = err.message || String(err); nameStatus.className = 'secret-name-status error'; saveBtn.textContent = t('viewSaveChanges'); + }).finally(function () { + saveBtn.disabled = false; + cancelBtn.disabled = false; + editBtn.disabled = false; }); }); @@ -1434,15 +1596,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option msg.textContent = t('viewNoSecrets'); viewBody.appendChild(msg); } - // Update table row - var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]'); - if (tableRow) { - var chip = tableRow.querySelector('.secret-chip'); - if (chip) { - var chipName = chip.querySelector('.secret-name'); - if (chipName && chipName.textContent === name) chip.remove(); - } - } + removeEntrySecretSchema(viewBody.getAttribute('data-entry-id'), secretId); }).catch(function (err) { alert(err.message || String(err)); });