refactor(entries): 将编辑弹窗中的密文管理功能移到查看密文弹窗
- 编辑弹窗移除密文区域(重命名、类型修改、解绑) - 查看密文弹窗增加:重命名(带 debounce 校验)、类型选择、解绑、保存 - 列表行密文 chips 保留只读展示,移除解绑按钮 - 简化编辑弹窗保存逻辑,不再处理密文变更
This commit is contained in:
@@ -416,6 +416,23 @@
|
||||
line-height: 1.6; text-align: center;
|
||||
}
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -506,7 +523,6 @@
|
||||
<span class="secret-chip">
|
||||
<span class="secret-name" title="{{ s.name }}">{{ s.name }}</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>
|
||||
{% endfor %}
|
||||
</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 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 modal-secrets"><label data-i18n="modalSecrets">密文</label><div id="edit-secrets-list" class="secret-list"></div></div>
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
@@ -661,6 +676,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewShow: '显示',
|
||||
viewHide: '隐藏',
|
||||
viewLoading: '解密中…',
|
||||
viewSaveChanges: '保存更改',
|
||||
viewChangesSaved: '已保存',
|
||||
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
|
||||
},
|
||||
'zh-TW': {
|
||||
pageTitle: 'Secrets — 條目',
|
||||
@@ -729,6 +747,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewShow: '顯示',
|
||||
viewHide: '隱藏',
|
||||
viewLoading: '解密中…',
|
||||
viewSaveChanges: '儲存變更',
|
||||
viewChangesSaved: '已儲存',
|
||||
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
|
||||
},
|
||||
en: {
|
||||
pageTitle: 'Secrets — Entries',
|
||||
@@ -797,6 +818,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewShow: 'Show',
|
||||
viewHide: 'Hide',
|
||||
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]));
|
||||
});
|
||||
});
|
||||
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');
|
||||
@@ -840,7 +855,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
var editTags = document.getElementById('edit-tags');
|
||||
var editMetadata = document.getElementById('edit-metadata');
|
||||
var editUpdatedAt = document.getElementById('edit-updated-at');
|
||||
var editSecretsList = document.getElementById('edit-secrets-list');
|
||||
var deleteOverlay = document.getElementById('delete-overlay');
|
||||
var deleteError = document.getElementById('delete-error');
|
||||
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();
|
||||
});
|
||||
|
||||
function renderViewSecrets(secrets) {
|
||||
function renderViewSecrets(secrets, secretSchema) {
|
||||
viewBody.innerHTML = '';
|
||||
var names = Object.keys(secrets);
|
||||
if (names.length === 0) {
|
||||
@@ -873,6 +887,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
viewBody.appendChild(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
var schemaMap = {};
|
||||
(secretSchema || []).forEach(function (s) { schemaMap[s.name] = s; });
|
||||
|
||||
names.forEach(function (name) {
|
||||
var raw = secrets[name];
|
||||
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 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');
|
||||
row.className = 'view-secret-row';
|
||||
row.setAttribute('data-secret-id', secretId);
|
||||
row.setAttribute('data-original-name', originalName);
|
||||
|
||||
var header = document.createElement('div');
|
||||
header.className = 'view-secret-header';
|
||||
|
||||
var nameWrap = document.createElement('div');
|
||||
nameWrap.className = 'view-secret-name-wrap';
|
||||
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'view-secret-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');
|
||||
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) {
|
||||
var toggleBtn = document.createElement('button');
|
||||
toggleBtn.type = 'button';
|
||||
@@ -919,6 +1001,16 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
});
|
||||
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);
|
||||
row.appendChild(header);
|
||||
|
||||
@@ -930,7 +1022,163 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
valueWrap.appendChild(valueEl);
|
||||
row.appendChild(valueWrap);
|
||||
|
||||
var nameStatus = document.createElement('div');
|
||||
nameStatus.className = 'secret-name-status';
|
||||
nameStatus.setAttribute('data-status', 'idle');
|
||||
row.appendChild(nameStatus);
|
||||
|
||||
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;
|
||||
viewBody.innerHTML = '';
|
||||
viewBody.setAttribute('data-entry-id', entryId);
|
||||
viewOverlay.hidden = false;
|
||||
|
||||
if (!encKey) {
|
||||
@@ -957,6 +1206,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
loadingMsg.textContent = t('viewLoading');
|
||||
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', {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Encryption-Key': encKey }
|
||||
@@ -966,7 +1219,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
return data;
|
||||
});
|
||||
}).then(function (data) {
|
||||
renderViewSecrets(data.secrets || {});
|
||||
renderViewSecrets(data.secrets || {}, secretSchema);
|
||||
}).catch(function (e) {
|
||||
viewBody.innerHTML = '';
|
||||
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' });
|
||||
}
|
||||
|
||||
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) {
|
||||
var id = tr.getAttribute('data-entry-id');
|
||||
if (!id) return;
|
||||
@@ -1177,12 +1264,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
} catch (err) {
|
||||
editMetadata.value = md;
|
||||
}
|
||||
var sj = tr.getAttribute('data-entry-secrets') || '[]';
|
||||
try {
|
||||
renderEditSecrets(JSON.parse(sj));
|
||||
} catch (err) {
|
||||
renderEditSecrets([]);
|
||||
}
|
||||
editOverlay.hidden = false;
|
||||
}
|
||||
|
||||
@@ -1190,7 +1271,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
editOverlay.hidden = true;
|
||||
currentEntryId = null;
|
||||
showEditErr('');
|
||||
editSecretsList.innerHTML = '';
|
||||
editUpdatedAt.textContent = '';
|
||||
editUpdatedAt.title = '';
|
||||
}
|
||||
@@ -1223,7 +1303,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
showDeleteErr('');
|
||||
}
|
||||
|
||||
function refreshListAfterSave(entryId, body, secretRows) {
|
||||
function refreshListAfterSave(entryId, body) {
|
||||
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
||||
if (!tr) { window.location.reload(); return; }
|
||||
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');
|
||||
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-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) {
|
||||
@@ -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);
|
||||
deleteOverlay.addEventListener('click', function (e) {
|
||||
if (e.target === deleteOverlay) closeDelete();
|
||||
@@ -1374,145 +1411,22 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
|
||||
tags: tags,
|
||||
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('');
|
||||
Promise.all(secretRows.map(function (info) {
|
||||
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',
|
||||
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'
|
||||
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 () {
|
||||
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));
|
||||
closeEdit();
|
||||
refreshListAfterSave(currentEntryId, body);
|
||||
}).catch(function (e) {
|
||||
showEditErr(e.message || String(e));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user