refactor(entries): 将编辑弹窗中的密文管理功能移到查看密文弹窗
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 6m6s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m36s

- 编辑弹窗移除密文区域(重命名、类型修改、解绑)
- 查看密文弹窗增加:重命名(带 debounce 校验)、类型选择、解绑、保存
- 列表行密文 chips 保留只读展示,移除解绑按钮
- 简化编辑弹窗保存逻辑,不再处理密文变更
This commit is contained in:
agent
2026-04-07 13:25:33 +08:00
parent a2a80a1744
commit 6fde982c20
2 changed files with 332 additions and 364 deletions

View File

@@ -416,6 +416,23 @@
line-height: 1.6; text-align: center; line-height: 1.6; text-align: center;
} }
.view-locked-msg a { color: var(--accent); } .view-locked-msg a { color: var(--accent); }
.view-secret-name-wrap { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex: 1; min-width: 0; }
.view-secret-name-input {
width: 180px; max-width: 100%; background: var(--bg); border: 1px solid var(--border);
border-radius: 4px; color: var(--text); padding: 2px 8px; font-size: 12px;
font-family: 'JetBrains Mono', monospace; outline: none;
}
.view-secret-name-input:focus { border-color: var(--accent); }
.view-secret-type-select {
background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
color: var(--text); padding: 2px 6px; font-size: 11px;
font-family: 'JetBrains Mono', monospace; outline: none; cursor: pointer;
}
.view-secret-type-select:focus { border-color: var(--accent); }
.btn-view-edit { color: var(--accent); }
.btn-view-save { color: #3fb950; }
.btn-view-cancel { color: var(--text-muted); }
.btn-view-unlink { color: #f85149; font-size: 14px; }
</style> </style>
</head> </head>
<body> <body>
@@ -506,7 +523,6 @@
<span class="secret-chip"> <span class="secret-chip">
<span class="secret-name" title="{{ s.name }}">{{ s.name }}</span> <span class="secret-name" title="{{ s.name }}">{{ s.name }}</span>
<span class="secret-type">{{ s.secret_type }}</span> <span class="secret-type">{{ s.secret_type }}</span>
<button type="button" class="btn-unlink-secret" data-secret-id="{{ s.id }}" data-secret-name="{{ s.name }}" title="解除关联">×</button>
</span> </span>
{% endfor %} {% endfor %}
</div> </div>
@@ -556,7 +572,6 @@
<div class="modal-field"><label for="edit-tags" data-i18n="modalTags">标签(逗号分隔)</label><input id="edit-tags" type="text" autocomplete="off"></div> <div class="modal-field"><label for="edit-tags" data-i18n="modalTags">标签(逗号分隔)</label><input id="edit-tags" type="text" autocomplete="off"></div>
<div class="modal-field"><label data-i18n="modalUpdated">更新</label><div id="edit-updated-at" class="modal-readonly-value" aria-live="polite"></div></div> <div class="modal-field"><label data-i18n="modalUpdated">更新</label><div id="edit-updated-at" class="modal-readonly-value" aria-live="polite"></div></div>
<div class="modal-field"><label for="edit-metadata" data-i18n="modalMetadata">元数据JSON 对象)</label><textarea id="edit-metadata" class="metadata-edit"></textarea></div> <div class="modal-field"><label for="edit-metadata" data-i18n="modalMetadata">元数据JSON 对象)</label><textarea id="edit-metadata" class="metadata-edit"></textarea></div>
<div class="modal-field modal-secrets"><label data-i18n="modalSecrets">密文</label><div id="edit-secrets-list" class="secret-list"></div></div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn-modal" id="edit-cancel" data-i18n="modalCancel">取消</button> <button type="button" class="btn-modal" id="edit-cancel" data-i18n="modalCancel">取消</button>
<button type="button" class="btn-modal primary" id="edit-save" data-i18n="modalSave">保存</button> <button type="button" class="btn-modal primary" id="edit-save" data-i18n="modalSave">保存</button>
@@ -661,6 +676,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewShow: '显示', viewShow: '显示',
viewHide: '隐藏', viewHide: '隐藏',
viewLoading: '解密中…', viewLoading: '解密中…',
viewSaveChanges: '保存更改',
viewChangesSaved: '已保存',
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
}, },
'zh-TW': { 'zh-TW': {
pageTitle: 'Secrets — 條目', pageTitle: 'Secrets — 條目',
@@ -729,6 +747,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewShow: '顯示', viewShow: '顯示',
viewHide: '隱藏', viewHide: '隱藏',
viewLoading: '解密中…', viewLoading: '解密中…',
viewSaveChanges: '儲存變更',
viewChangesSaved: '已儲存',
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
}, },
en: { en: {
pageTitle: 'Secrets — Entries', pageTitle: 'Secrets — Entries',
@@ -797,6 +818,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewShow: 'Show', viewShow: 'Show',
viewHide: 'Hide', viewHide: 'Hide',
viewLoading: 'Decrypting…', viewLoading: 'Decrypting…',
viewSaveChanges: 'Save changes',
viewChangesSaved: 'Saved',
viewUnlinkConfirm: 'Unlink secret "{name}"?',
} }
}; };
@@ -820,15 +844,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
if (td) td.setAttribute('data-label', t(map[sel])); if (td) td.setAttribute('data-label', t(map[sel]));
}); });
}); });
editSecretsList.querySelectorAll('.btn-unlink-secret').forEach(function (btn) {
btn.title = t('unlinkTitle');
});
editSecretsList.querySelectorAll('.secret-name-input').forEach(function (input) {
input.placeholder = t('renameSecretPlaceholder');
});
document.querySelectorAll('.table-wrap .btn-unlink-secret').forEach(function (btn) {
btn.title = t('unlinkTitle');
});
}; };
var editOverlay = document.getElementById('edit-overlay'); var editOverlay = document.getElementById('edit-overlay');
@@ -840,7 +855,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var editTags = document.getElementById('edit-tags'); var editTags = document.getElementById('edit-tags');
var editMetadata = document.getElementById('edit-metadata'); var editMetadata = document.getElementById('edit-metadata');
var editUpdatedAt = document.getElementById('edit-updated-at'); var editUpdatedAt = document.getElementById('edit-updated-at');
var editSecretsList = document.getElementById('edit-secrets-list');
var deleteOverlay = document.getElementById('delete-overlay'); var deleteOverlay = document.getElementById('delete-overlay');
var deleteError = document.getElementById('delete-error'); var deleteError = document.getElementById('delete-error');
var deleteMessage = document.getElementById('delete-message'); var deleteMessage = document.getElementById('delete-message');
@@ -863,7 +877,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
if (e.target === viewOverlay) closeView(); if (e.target === viewOverlay) closeView();
}); });
function renderViewSecrets(secrets) { function renderViewSecrets(secrets, secretSchema) {
viewBody.innerHTML = ''; viewBody.innerHTML = '';
var names = Object.keys(secrets); var names = Object.keys(secrets);
if (names.length === 0) { if (names.length === 0) {
@@ -873,6 +887,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewBody.appendChild(msg); viewBody.appendChild(msg);
return; return;
} }
var schemaMap = {};
(secretSchema || []).forEach(function (s) { schemaMap[s.name] = s; });
names.forEach(function (name) { names.forEach(function (name) {
var raw = secrets[name]; var raw = secrets[name];
var valueStr = (raw === null || raw === undefined) ? '' : var valueStr = (raw === null || raw === undefined) ? '' :
@@ -880,20 +898,84 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var isPassword = (name === 'password' || name === 'passwd' || name === 'secret'); var isPassword = (name === 'password' || name === 'passwd' || name === 'secret');
var masked = isPassword; var masked = isPassword;
var schema = schemaMap[name] || {};
var secretId = schema.id || '';
var secretType = schema.secret_type || 'text';
var originalName = name;
var hasChanges = false;
var row = document.createElement('div'); var row = document.createElement('div');
row.className = 'view-secret-row'; row.className = 'view-secret-row';
row.setAttribute('data-secret-id', secretId);
row.setAttribute('data-original-name', originalName);
var header = document.createElement('div'); var header = document.createElement('div');
header.className = 'view-secret-header'; header.className = 'view-secret-header';
var nameWrap = document.createElement('div');
nameWrap.className = 'view-secret-name-wrap';
var nameSpan = document.createElement('span'); var nameSpan = document.createElement('span');
nameSpan.className = 'view-secret-name'; nameSpan.className = 'view-secret-name';
nameSpan.textContent = name; nameSpan.textContent = name;
header.appendChild(nameSpan);
var nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'view-secret-name-input';
nameInput.value = name;
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;
var typeSelect = document.createElement('select');
typeSelect.className = 'view-secret-type-select';
typeSelect.hidden = true;
SECRET_TYPE_OPTIONS.forEach(function (opt) {
var option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (opt === secretType) option.selected = true;
typeSelect.appendChild(option);
});
if (SECRET_TYPE_OPTIONS.indexOf(secretType) === -1 && secretType) {
var fallback = document.createElement('option');
fallback.value = secretType;
fallback.textContent = secretType;
fallback.selected = true;
typeSelect.appendChild(fallback);
}
nameWrap.appendChild(nameSpan);
nameWrap.appendChild(nameInput);
nameWrap.appendChild(typeBadge);
nameWrap.appendChild(typeSelect);
header.appendChild(nameWrap);
var actions = document.createElement('div'); var actions = document.createElement('div');
actions.className = 'view-secret-actions'; actions.className = 'view-secret-actions';
var editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'btn-icon btn-view-edit';
editBtn.textContent = '✎';
editBtn.title = t('renameSecretTitle');
var saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'btn-icon btn-view-save';
saveBtn.textContent = t('viewSaveChanges');
saveBtn.hidden = true;
var cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'btn-icon btn-view-cancel';
cancelBtn.textContent = t('modalCancel');
cancelBtn.hidden = true;
if (isPassword) { if (isPassword) {
var toggleBtn = document.createElement('button'); var toggleBtn = document.createElement('button');
toggleBtn.type = 'button'; toggleBtn.type = 'button';
@@ -919,6 +1001,16 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
}); });
actions.appendChild(copyBtn); actions.appendChild(copyBtn);
var unlinkBtn = document.createElement('button');
unlinkBtn.type = 'button';
unlinkBtn.className = 'btn-icon btn-view-unlink';
unlinkBtn.textContent = '×';
unlinkBtn.title = t('unlinkTitle');
actions.appendChild(unlinkBtn);
actions.appendChild(editBtn);
actions.appendChild(saveBtn);
actions.appendChild(cancelBtn);
header.appendChild(actions); header.appendChild(actions);
row.appendChild(header); row.appendChild(header);
@@ -930,7 +1022,163 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
valueWrap.appendChild(valueEl); valueWrap.appendChild(valueEl);
row.appendChild(valueWrap); row.appendChild(valueWrap);
var nameStatus = document.createElement('div');
nameStatus.className = 'secret-name-status';
nameStatus.setAttribute('data-status', 'idle');
row.appendChild(nameStatus);
viewBody.appendChild(row); viewBody.appendChild(row);
// ── Edit mode toggle ──
function enterEditMode() {
nameSpan.hidden = true;
typeBadge.hidden = true;
nameInput.hidden = false;
typeSelect.hidden = false;
saveBtn.hidden = false;
cancelBtn.hidden = false;
editBtn.hidden = true;
nameInput.focus();
nameInput.select();
}
function exitEditMode() {
nameSpan.hidden = false;
typeBadge.hidden = false;
nameInput.hidden = true;
typeSelect.hidden = true;
saveBtn.hidden = true;
cancelBtn.hidden = true;
editBtn.hidden = false;
nameStatus.textContent = '';
nameStatus.className = 'secret-name-status';
nameInput.value = nameSpan.textContent;
typeSelect.value = typeBadge.textContent;
hasChanges = false;
}
editBtn.addEventListener('click', enterEditMode);
cancelBtn.addEventListener('click', exitEditMode);
// ── Name validation ──
var debounceTimer = null;
var currentCheck = null;
nameInput.addEventListener('input', function () {
if (debounceTimer) clearTimeout(debounceTimer);
nameStatus.textContent = '';
nameStatus.className = 'secret-name-status';
debounceTimer = setTimeout(function () {
var newName = nameInput.value.trim();
if (!newName || newName === originalName) return;
nameStatus.textContent = t('checkingSecretName');
nameStatus.className = 'secret-name-status checking';
var checkId = Date.now();
currentCheck = checkId;
var params = new URLSearchParams();
params.set('name', newName);
params.set('exclude_secret_id', secretId);
fetch('/api/secrets/check-name?' + params.toString(), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (currentCheck !== checkId) return;
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);
});
nameInput.addEventListener('keydown', function (e) {
if (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;
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; }
var patchBody = {};
if (newName !== originalName) patchBody.name = newName;
if (newType !== secretType) patchBody.type = newType;
if (Object.keys(patchBody).length === 0) { exitEditMode(); return; }
saveBtn.textContent = '...';
fetch('/api/secrets/' + encodeURIComponent(secretId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(patchBody)
}).then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
}).then(function () {
nameSpan.textContent = newName;
typeBadge.textContent = newType;
originalName = newName;
nameInput.setAttribute('data-original-name', newName);
saveBtn.textContent = t('viewChangesSaved');
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');
});
});
// ── Unlink ──
unlinkBtn.addEventListener('click', function () {
if (!confirm(tf('viewUnlinkConfirm', { name: nameSpan.textContent }))) return;
fetch('/api/entries/' + encodeURIComponent(viewBody.getAttribute('data-entry-id')) + '/secrets/' + encodeURIComponent(secretId), {
method: 'DELETE',
credentials: 'same-origin'
}).then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
}).then(function () {
row.remove();
if (!viewBody.querySelector('.view-secret-row')) {
viewBody.innerHTML = '';
var msg = document.createElement('div');
msg.className = 'view-locked-msg';
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();
}
}
}).catch(function (err) {
alert(err.message || String(err));
});
});
}); });
} }
@@ -942,6 +1190,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewEntryName.textContent = entryName; viewEntryName.textContent = entryName;
viewBody.innerHTML = ''; viewBody.innerHTML = '';
viewBody.setAttribute('data-entry-id', entryId);
viewOverlay.hidden = false; viewOverlay.hidden = false;
if (!encKey) { if (!encKey) {
@@ -957,6 +1206,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
loadingMsg.textContent = t('viewLoading'); loadingMsg.textContent = t('viewLoading');
viewBody.appendChild(loadingMsg); viewBody.appendChild(loadingMsg);
var sj = tr.getAttribute('data-entry-secrets') || '[]';
var secretSchema;
try { secretSchema = JSON.parse(sj); } catch (e) { secretSchema = []; }
fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/decrypt', { fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/decrypt', {
credentials: 'same-origin', credentials: 'same-origin',
headers: { 'X-Encryption-Key': encKey } headers: { 'X-Encryption-Key': encKey }
@@ -966,7 +1219,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
return data; return data;
}); });
}).then(function (data) { }).then(function (data) {
renderViewSecrets(data.secrets || {}); renderViewSecrets(data.secrets || {}, secretSchema);
}).catch(function (e) { }).catch(function (e) {
viewBody.innerHTML = ''; viewBody.innerHTML = '';
var errMsg = document.createElement('div'); var errMsg = document.createElement('div');
@@ -990,172 +1243,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium' }); return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium' });
} }
function renderEditSecrets(secrets) {
editSecretsList.innerHTML = '';
if (!Array.isArray(secrets) || secrets.length === 0) return;
secrets.forEach(function (s) {
var row = document.createElement('div');
row.className = 'secret-edit-row';
row.setAttribute('data-secret-id', s.id || '');
row.setAttribute('data-secret-name', s.name || '');
row.setAttribute('data-secret-type', s.secret_type || '');
var main = document.createElement('div');
main.className = 'secret-edit-main';
var input = document.createElement('input');
input.type = 'text';
input.className = 'secret-name-input';
input.value = s.name || '';
input.placeholder = t('renameSecretPlaceholder');
input.setAttribute('data-original-name', s.name || '');
var typeSelect = document.createElement('select');
typeSelect.className = 'secret-type-select';
var currentType = s.secret_type || 'text';
var hasCurrentInOptions = SECRET_TYPE_OPTIONS.indexOf(currentType) !== -1;
SECRET_TYPE_OPTIONS.forEach(function (opt) {
var option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (opt === currentType) option.selected = true;
typeSelect.appendChild(option);
});
if (!hasCurrentInOptions && currentType) {
var fallback = document.createElement('option');
fallback.value = currentType;
fallback.textContent = currentType;
fallback.selected = true;
typeSelect.appendChild(fallback);
}
var unlinkBtn = document.createElement('button');
unlinkBtn.type = 'button';
unlinkBtn.className = 'btn-unlink-secret';
unlinkBtn.setAttribute('data-secret-id', s.id || '');
unlinkBtn.setAttribute('data-secret-name', s.name || '');
unlinkBtn.title = t('unlinkTitle');
unlinkBtn.textContent = '\u00d7';
main.appendChild(typeSelect);
main.appendChild(input);
main.appendChild(unlinkBtn);
var status = document.createElement('div');
status.className = 'secret-name-status';
status.setAttribute('data-status', 'idle');
row.appendChild(main);
row.appendChild(status);
editSecretsList.appendChild(row);
bindSecretValidation(row, s.id, s.name, s.secret_type || '');
});
}
function bindSecretValidation(row, secretId, originalName, originalType) {
var input = row.querySelector('.secret-name-input');
var typeSelect = row.querySelector('.secret-type-select');
var status = row.querySelector('.secret-name-status');
var debounceTimer = null;
var currentCheck = null;
var lastValidatedName = originalName;
function setStatus(text, type) {
status.textContent = text || '';
status.className = 'secret-name-status';
if (type) status.classList.add(type);
row.setAttribute('data-validation-state', type || 'idle');
}
function setLastValidatedName(name) {
row.setAttribute('data-last-validated-name', name || '');
}
function invalidateValidationState() {
currentCheck = null;
lastValidatedName = null;
setLastValidatedName('');
setStatus('', 'idle');
input.className = 'secret-name-input';
}
function checkNameAvailability(name) {
if (!name || name === originalName) {
setStatus('', 'idle');
input.className = 'secret-name-input';
setLastValidatedName(originalName);
return Promise.resolve(false);
}
if (name.length > 256) {
setStatus(t('secretNameInvalid'), 'error');
input.className = 'secret-name-input invalid';
setLastValidatedName('');
return Promise.resolve(false);
}
setStatus(t('checkingSecretName'), 'checking');
input.className = 'secret-name-input';
var checkId = Date.now();
currentCheck = checkId;
var params = new URLSearchParams();
params.set('name', name);
params.set('exclude_secret_id', secretId);
return fetch('/api/secrets/check-name?' + params.toString(), {
credentials: 'same-origin'
}).then(function (r) {
return r.json();
}).then(function (data) {
if (currentCheck !== checkId) return false;
if (data.ok && data.available) {
setStatus(t('secretNameAvailable'), 'success');
input.className = 'secret-name-input valid';
lastValidatedName = name;
setLastValidatedName(name);
return true;
} else {
setStatus(data.error || t('secretNameTaken'), 'error');
input.className = 'secret-name-input invalid';
setLastValidatedName('');
return false;
}
}).catch(function () {
if (currentCheck !== checkId) return false;
setStatus(t('secretNameCheckError'), 'error');
input.className = 'secret-name-input invalid';
setLastValidatedName('');
return false;
});
}
row.secretValidateNow = function () {
return checkNameAvailability(input.value.trim());
};
input.addEventListener('input', function () {
var newName = input.value.trim();
if (debounceTimer) clearTimeout(debounceTimer);
invalidateValidationState();
debounceTimer = setTimeout(function () {
checkNameAvailability(newName);
}, 300);
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
checkNameAvailability(input.value.trim());
} else if (e.key === 'Escape') {
input.value = originalName;
invalidateValidationState();
}
});
}
function openEdit(tr) { function openEdit(tr) {
var id = tr.getAttribute('data-entry-id'); var id = tr.getAttribute('data-entry-id');
if (!id) return; if (!id) return;
@@ -1177,12 +1264,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
} catch (err) { } catch (err) {
editMetadata.value = md; editMetadata.value = md;
} }
var sj = tr.getAttribute('data-entry-secrets') || '[]';
try {
renderEditSecrets(JSON.parse(sj));
} catch (err) {
renderEditSecrets([]);
}
editOverlay.hidden = false; editOverlay.hidden = false;
} }
@@ -1190,7 +1271,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
editOverlay.hidden = true; editOverlay.hidden = true;
currentEntryId = null; currentEntryId = null;
showEditErr(''); showEditErr('');
editSecretsList.innerHTML = '';
editUpdatedAt.textContent = ''; editUpdatedAt.textContent = '';
editUpdatedAt.title = ''; editUpdatedAt.title = '';
} }
@@ -1223,7 +1303,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
showDeleteErr(''); showDeleteErr('');
} }
function refreshListAfterSave(entryId, body, secretRows) { function refreshListAfterSave(entryId, body) {
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]'); var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
if (!tr) { window.location.reload(); return; } if (!tr) { window.location.reload(); return; }
var nameCell = tr.querySelector('.cell-name'); var nameCell = tr.querySelector('.cell-name');
@@ -1237,38 +1317,8 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
} }
var tagsCell = tr.querySelector('.cell-tags-val'); var tagsCell = tr.querySelector('.cell-tags-val');
if (tagsCell) tagsCell.textContent = body.tags.join(', '); if (tagsCell) tagsCell.textContent = body.tags.join(', ');
var secretsList = tr.querySelector('.secret-list');
if (secretsList) {
secretsList.innerHTML = '';
secretRows.forEach(function (info) {
var chip = document.createElement('span');
chip.className = 'secret-chip';
var nameSpan = document.createElement('span');
nameSpan.className = 'secret-name';
nameSpan.textContent = info.newName;
nameSpan.title = info.newName;
var typeSpan = document.createElement('span');
typeSpan.className = 'secret-type';
typeSpan.textContent = info.newType || 'text';
var unlinkBtn = document.createElement('button');
unlinkBtn.type = 'button';
unlinkBtn.className = 'btn-unlink-secret';
unlinkBtn.setAttribute('data-secret-id', info.secretId);
unlinkBtn.setAttribute('data-secret-name', info.newName);
unlinkBtn.title = t('unlinkTitle');
unlinkBtn.textContent = '\u00d7';
chip.appendChild(nameSpan);
chip.appendChild(typeSpan);
chip.appendChild(unlinkBtn);
secretsList.appendChild(chip);
});
}
tr.setAttribute('data-entry-folder', body.folder); tr.setAttribute('data-entry-folder', body.folder);
tr.setAttribute('data-entry-metadata', JSON.stringify(body.metadata)); tr.setAttribute('data-entry-metadata', JSON.stringify(body.metadata));
var updatedSecrets = secretRows.map(function (info) {
return { id: info.secretId, name: info.newName, secret_type: info.newType || 'text' };
});
tr.setAttribute('data-entry-secrets', JSON.stringify(updatedSecrets));
} }
function refreshListAfterDelete(entryId) { function refreshListAfterDelete(entryId) {
@@ -1318,19 +1368,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
} }
} }
function refreshListAfterUnlink(entryId, secretId) {
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
if (!tr) return;
var chip = tr.querySelector('.btn-unlink-secret[data-secret-id="' + secretId + '"]');
if (chip && chip.parentElement) chip.parentElement.remove();
var secrets = tr.getAttribute('data-entry-secrets');
try {
var arr = JSON.parse(secrets);
arr = arr.filter(function (s) { return s.id !== secretId; });
tr.setAttribute('data-entry-secrets', JSON.stringify(arr));
} catch (e) {}
}
document.getElementById('delete-cancel').addEventListener('click', closeDelete); document.getElementById('delete-cancel').addEventListener('click', closeDelete);
deleteOverlay.addEventListener('click', function (e) { deleteOverlay.addEventListener('click', function (e) {
if (e.target === deleteOverlay) closeDelete(); if (e.target === deleteOverlay) closeDelete();
@@ -1374,145 +1411,22 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
tags: tags, tags: tags,
metadata: meta metadata: meta
}; };
var secretRows = Array.from(editSecretsList.querySelectorAll('.secret-edit-row')).map(function (row) {
var input = row.querySelector('.secret-name-input');
var typeSelect = row.querySelector('.secret-type-select');
return {
row: row,
input: input,
typeSelect: typeSelect,
secretId: row.getAttribute('data-secret-id') || '',
originalName: input ? (input.getAttribute('data-original-name') || '') : '',
newName: input ? input.value.trim() : '',
originalType: row.getAttribute('data-secret-type') || '',
newType: typeSelect ? typeSelect.value : ''
};
});
showEditErr(''); showEditErr('');
Promise.all(secretRows.map(function (info) { fetch('/api/entries/' + encodeURIComponent(currentEntryId), {
if (!info.secretId || info.newName === info.originalName) return Promise.resolve(true); method: 'PATCH',
if (typeof info.row.secretValidateNow === 'function') { headers: { 'Content-Type': 'application/json' },
return info.row.secretValidateNow(); credentials: 'same-origin',
} body: JSON.stringify(body)
return Promise.resolve(false);
})).then(function () {
var invalidSecret = secretRows.find(function (info) {
if (!info.secretId || info.newName === info.originalName) return false;
var state = info.row.getAttribute('data-validation-state') || 'idle';
var lastValidated = info.row.getAttribute('data-last-validated-name') || '';
return state !== 'success' || lastValidated !== info.newName;
});
if (invalidSecret) {
if (invalidSecret.input) invalidSecret.input.focus();
throw new Error(t('secretNameFixBeforeSave'));
}
return fetch('/api/entries/' + encodeURIComponent(currentEntryId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(body)
}).then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
});
}).then(function () {
var changedSecrets = secretRows.filter(function (info) {
return info.secretId && (info.newName !== info.originalName || info.newType !== info.originalType);
});
return Promise.all(changedSecrets.map(function (info) {
var patchBody = {};
if (info.newName !== info.originalName) patchBody.name = info.newName;
if (info.newType !== info.originalType) patchBody.type = info.newType;
return fetch('/api/secrets/' + encodeURIComponent(info.secretId), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(patchBody)
}).then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
}).catch(function (err) {
var msg = err.message || String(err);
var status = info.row.querySelector('.secret-name-status');
info.row.setAttribute('data-validation-state', 'error');
info.row.setAttribute('data-last-validated-name', '');
if (status) {
status.textContent = msg;
status.className = 'secret-name-status error';
}
if (info.input) {
info.input.className = 'secret-name-input invalid';
}
if (info.typeSelect) {
info.typeSelect.className = 'secret-type-select invalid';
}
if (info.input) info.input.focus();
throw new Error(tf('errRenameSecret', { error: msg }));
});
}));
}).then(function () {
closeEdit();
refreshListAfterSave(currentEntryId, body, secretRows);
}).catch(function (e) {
showEditErr(e.message || String(e));
});
});
var tableWrap = document.querySelector('.table-wrap');
if (tableWrap) {
tableWrap.addEventListener('click', function (e) {
var btn = e.target.closest('.btn-unlink-secret');
if (!btn || !tableWrap.contains(btn)) return;
var tr = btn.closest('tr[data-entry-id]');
var entryId = tr && tr.getAttribute('data-entry-id');
var secretId = btn.getAttribute('data-secret-id');
var secretName = btn.getAttribute('data-secret-name') || '';
if (!entryId || !secretId) return;
fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/' + encodeURIComponent(secretId), {
method: 'DELETE',
credentials: 'same-origin'
}).then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
}).then(function () {
refreshListAfterUnlink(entryId, secretId);
}).catch(function (err) {
alert(err.message || String(err));
});
});
}
editSecretsList.addEventListener('click', function (e) {
var btn = e.target.closest('.btn-unlink-secret');
if (!btn || !editSecretsList.contains(btn)) return;
var entryId = currentEntryId;
var secretId = btn.getAttribute('data-secret-id');
var secretName = btn.getAttribute('data-secret-name') || '';
if (!entryId || !secretId) return;
fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/' + encodeURIComponent(secretId), {
method: 'DELETE',
credentials: 'same-origin'
}).then(function (r) { }).then(function (r) {
return r.json().then(function (data) { return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status)); if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data; return data;
}); });
}).then(function () { }).then(function () {
btn.closest('.secret-edit-row').remove(); closeEdit();
var tableRow = document.querySelector('tr[data-entry-id="' + entryId + '"]'); refreshListAfterSave(currentEntryId, body);
if (tableRow) { }).catch(function (e) {
var chip = tableRow.querySelector('.btn-unlink-secret[data-secret-id="' + secretId + '"]'); showEditErr(e.message || String(e));
if (chip && chip.parentElement) chip.parentElement.remove();
}
}).catch(function (err) {
alert(err.message || String(err));
}); });
}); });

View File

@@ -0,0 +1,54 @@
# 将编辑弹窗中的密文管理功能移到查看密文弹窗
## 当前状态
- **编辑弹窗**密文重命名input、类型修改select、解绑×按钮、name 可用性校验
- **查看密文弹窗**:解密后显示值、复制、显示/隐藏密码
- **列表行**:密文 chipsname+type+ 解绑按钮
## 变更内容
### 1. 编辑弹窗 — 移除密文区域
- 移除 HTML 中 `#edit-secrets-list` 所在的 `.modal-secrets` div第559行
- 移除 JS 中 `renderEditSecrets``bindSecretValidation` 函数
- 移除 `openEdit` 中读取/渲染 `data-entry-secrets` 的逻辑
- 移除 `edit-save` 中 secret rename/type PATCH 逻辑
- 移除编辑弹窗内的 unlink 事件监听器第1492-1517行
- `refreshListAfterSave` 不再处理 secretRows 参数
### 2. 查看密文弹窗 — 增加管理功能
在每个解密字段行中增加:
- **重命名输入框**inline edit带 debounce 校验)
- **类型下拉选择**
- **解绑按钮**
- **保存按钮**(逐行或统一保存)
- 复用现有的 `PATCH /api/secrets/{id}``DELETE /api/entries/{entry_id}/secrets/{secret_id}` 接口
需要在 `openView` 中额外传入 `data-entry-secrets`(含 secret id/name/type以便将管理功能与解密值关联。
### 3. 列表行 — 保留只读摘要
- 保留密文 chips 的 name + type 展示
- **移除** chips 上的解绑按钮(×)
- **移除**列表行的 unlink 事件监听器第1466-1490行
### 4. i18n 更新
- 为查看弹窗新增重命名、类型修改、解绑相关的中英文翻译
- 清理编辑弹窗中不再需要的 i18n key
### 5. CSS 调整
- 查看弹窗中为管理控件添加样式input/select/button 行内布局)
## 不涉及的变更
- 后端 API 无需修改(复用现有接口)
- 版本 bump视为前次 0.5.11 的一部分tag 尚未被 CI 创建)
## 涉及文件
- `crates/secrets-mcp/templates/entries.html`HTML + JS + CSS
- `crates/secrets-mcp/src/web/entries.rs`(无需修改,复用现有 API