@@ -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 : 6 px ; flex-wrap : wrap ; flex : 1 ; min-width : 0 ; }
. view-secret-name-input {
width : 180 px ; max-width : 100 % ; background : var ( - - bg ) ; border : 1 px solid var ( - - border ) ;
border-radius : 4 px ; color : var ( - - text ) ; padding : 2 px 8 px ; font-size : 12 px ;
font-family : 'JetBrains Mono' , monospace ; outline : none ;
}
. view-secret-name-input : focus { border-color : var ( - - accent ) ; }
. view-secret-type-select {
background : var ( - - surface2 ) ; border : 1 px solid var ( - - border ) ; border-radius : 4 px ;
color : var ( - - text ) ; padding : 2 px 6 px ; font-size : 11 px ;
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 : 14 px ; }
< / 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 ) ) ;
} ) ;
} ) ;