refactor(entries): 将编辑弹窗中的密文管理功能移到查看密文弹窗
- 编辑弹窗移除密文区域(重命名、类型修改、解绑) - 查看密文弹窗增加:重命名(带 debounce 校验)、类型选择、解绑、保存 - 列表行密文 chips 保留只读展示,移除解绑按钮 - 简化编辑弹窗保存逻辑,不再处理密文变更
This commit is contained in:
@@ -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,40 +1411,8 @@ 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);
|
|
||||||
if (typeof info.row.secretValidateNow === 'function') {
|
|
||||||
return info.row.secretValidateNow();
|
|
||||||
}
|
|
||||||
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',
|
method: 'PATCH',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -1417,105 +1422,14 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
|||||||
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 () {
|
|
||||||
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 () {
|
}).then(function () {
|
||||||
closeEdit();
|
closeEdit();
|
||||||
refreshListAfterSave(currentEntryId, body, secretRows);
|
refreshListAfterSave(currentEntryId, body);
|
||||||
}).catch(function (e) {
|
}).catch(function (e) {
|
||||||
showEditErr(e.message || String(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) {
|
|
||||||
return r.json().then(function (data) {
|
|
||||||
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
}).then(function () {
|
|
||||||
btn.closest('.secret-edit-row').remove();
|
|
||||||
var tableRow = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
|
||||||
if (tableRow) {
|
|
||||||
var chip = tableRow.querySelector('.btn-unlink-secret[data-secret-id="' + secretId + '"]');
|
|
||||||
if (chip && chip.parentElement) chip.parentElement.remove();
|
|
||||||
}
|
|
||||||
}).catch(function (err) {
|
|
||||||
alert(err.message || String(err));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
|
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
|
||||||
var viewBtn = tr.querySelector('.btn-view-secrets');
|
var viewBtn = tr.querySelector('.btn-view-secrets');
|
||||||
if (viewBtn) {
|
if (viewBtn) {
|
||||||
|
|||||||
54
plans/move-secret-management-to-view.md
Normal file
54
plans/move-secret-management-to-view.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# 将编辑弹窗中的密文管理功能移到查看密文弹窗
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
- **编辑弹窗**:密文重命名(input)、类型修改(select)、解绑(×按钮)、name 可用性校验
|
||||||
|
- **查看密文弹窗**:解密后显示值、复制、显示/隐藏密码
|
||||||
|
- **列表行**:密文 chips(name+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)
|
||||||
Reference in New Issue
Block a user