@@ -145,12 +145,13 @@
}
table {
width : 100 % ;
min-width : 110 0 px ;
min-width : 124 0 px ;
border-collapse : separate ;
border-spacing : 0 ;
table-layout : fixed ;
}
col [ data-col = "name" ] { width : 220 px ; }
col [ data-col = "folder" ] { width : 140 px ; }
col [ data-col = "type" ] { width : 120 px ; }
col [ data-col = "notes" ] { width : 320 px ; }
col [ data-col = "tags" ] { width : 220 px ; }
@@ -172,8 +173,8 @@
}
td { font-size : 13 px ; line-height : 1.45 ; color : #c9d1d9 ; }
tbody tr : nth-child ( 2n ) td { background : rgba ( 255 , 255 , 255 , 0.01 ) ; }
tbody tr : nth-child ( 2n ) td . col-name { background : #0f1620 ; }
. mono { font-family : 'JetBrains Mono' , monospace ; }
. col-folder { text-align : center ; vertical-align : middle ; }
. col-type { text-align : center ; vertical-align : middle ; }
. col-secrets { vertical-align : middle ; }
. col-secrets . secret-list { max-height : 120 px ; overflow : auto ; }
@@ -456,7 +457,16 @@
padding : 7 px 10 px ; word-break : break-all ; white-space : pre-wrap ;
max-height : 140 px ; overflow : auto ; color : #c9d1d9 ; line-height : 1.5 ;
}
. view-secret-editor {
width : 100 % ; min-height : 108 px ; resize : vertical ; box-sizing : border-box ;
font-family : 'JetBrains Mono' , monospace ; font-size : 12 px ; line-height : 1.5 ;
background : #0d1117 ; border : 1 px solid rgba ( 240 , 246 , 252 , 0.08 ) ; border-radius : 10 px ;
color : #c9d1d9 ; padding : 10 px 12 px ; outline : none ;
}
. view-secret-editor : focus { border-color : rgba ( 56 , 139 , 253 , 0.5 ) ; }
. view-secret-hint {
margin-top : 6 px ; font-size : 12 px ; color : #8b949e ; line-height : 1.5 ;
}
. btn-icon {
padding : 6 px 10 px ; border-radius : 8 px ; font-size : 12 px ; cursor : pointer ;
border : 1 px solid rgba ( 240 , 246 , 252 , 0.12 ) ; background : #161b22 ; color : #8b949e ;
@@ -484,7 +494,7 @@
. btn-view-edit { color : #58a6ff ; }
. btn-view-save { color : #3fb950 ; }
. btn-view-cancel { color : #8b949e ; }
. btn-view-unlink { color : #f85149 ; font-size : 14 px ; }
. btn-view-unlink { color : #f85149 ; font-size : 12 px ; white-space : nowrap ; }
< / style >
< / head >
< body >
@@ -548,7 +558,7 @@
< / div >
< div class = "filter-actions" >
< button type = "submit" class = "btn-filter" data-i18n = "filterSubmit" > 筛选< / button >
< a href = "/entries" class = "btn-clear" data-i18n = "filterClear" > 清空 < / a >
< a href = "/entries" class = "btn-clear" data-i18n = "filterClear" > 重置 < / a >
< div class = "col-menu" >
< button type = "button" class = "btn-col-toggle" id = "col-toggle-btn" data-i18n-title = "columnSettings" title = "显示列" > ▥< / button >
< div class = "col-panel" id = "col-panel" > < / div >
@@ -563,6 +573,7 @@
< table >
< colgroup >
< col data-col = "name" >
< col data-col = "folder" >
< col data-col = "type" >
< col data-col = "notes" >
< col data-col = "tags" >
@@ -573,6 +584,7 @@
< thead >
< tr >
< th data-col = "name" data-i18n = "colName" > 名称< / th >
< th data-col = "folder" data-i18n = "colFolder" > 文件夹< / th >
< th data-col = "type" data-i18n = "colType" > 类型< / th >
< th data-col = "notes" data-i18n = "colNotes" > 备注< / th >
< th data-col = "tags" data-i18n = "colTags" > 标签< / th >
@@ -585,6 +597,7 @@
{% for entry in entries %}
< tr data-entry-id = "{{ entry.id }}" data-entry-folder = "{{ entry.folder }}" data-entry-metadata = "{{ entry.metadata_json }}" data-entry-secrets = "{{ entry.secrets_json }}" data-entry-parents = "{{ entry.parents_json }}" data-updated-at = "{{ entry.updated_at_iso }}" >
< td class = "col-name mono cell-name" data-col = "name" data-label = "名称" > {{ entry.name }}< / td >
< td class = "col-folder mono cell-folder" data-col = "folder" data-label = "文件夹" > {{ entry.folder }}< / td >
< td class = "col-type mono cell-type" data-col = "type" data-label = "类型" > {{ entry.entry_type }}< / td >
< td class = "col-notes cell-notes" data-col = "notes" data-label = "备注" > {% if !entry.notes.is_empty() %}< div class = "notes-scroll cell-notes-val" > {{ entry.notes }}< / div > {% endif %}< / td >
< td class = "col-tags mono cell-tags-val" data-col = "tags" data-label = "标签" > {{ entry.tags }}< / td >
@@ -705,9 +718,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
filterTypeLabel : '类型' ,
filterTypeAll : '全部' ,
filterSubmit : '筛选' ,
filterClear : '清空 ' ,
filterClear : '重置 ' ,
emptyEntries : '暂无条目。' ,
colName : '名称' ,
colFolder : '文件夹' ,
colType : '类型' ,
colNotes : '备注' ,
colTags : '标签' ,
@@ -731,6 +745,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalCancel : '取消' ,
modalSave : '保存' ,
mobileLabelName : '名称' ,
mobileLabelFolder : '文件夹' ,
mobileLabelType : '类型' ,
mobileLabelNotes : '备注' ,
mobileLabelTags : '标签' ,
@@ -763,6 +778,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewCopy : '复制' ,
viewCopied : '已复制' ,
viewLoading : '解密中…' ,
viewEditSecret : '编辑密文' ,
viewValueHintJson : '此值按 JSON 保存,请输入合法 JSON。' ,
viewValueInvalidJson : '请输入合法 JSON 值' ,
viewValueUnlockRequired : '请先在 MCP 配置页解锁密码短语后再修改密文值。' ,
viewSaveChanges : '保存更改' ,
viewChangesSaved : '已保存' ,
viewUnlinkConfirm : '确定解除密文关联「{name}」?' ,
@@ -785,9 +804,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
filterTypeLabel : '類型' ,
filterTypeAll : '全部' ,
filterSubmit : '篩選' ,
filterClear : '清除 ' ,
filterClear : '重置 ' ,
emptyEntries : '暫無條目。' ,
colName : '名稱' ,
colFolder : '資料夾' ,
colType : '類型' ,
colNotes : '備註' ,
colTags : '標籤' ,
@@ -811,6 +831,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalCancel : '取消' ,
modalSave : '儲存' ,
mobileLabelName : '名稱' ,
mobileLabelFolder : '資料夾' ,
mobileLabelType : '類型' ,
mobileLabelNotes : '備註' ,
mobileLabelTags : '標籤' ,
@@ -843,6 +864,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewCopy : '複製' ,
viewCopied : '已複製' ,
viewLoading : '解密中…' ,
viewEditSecret : '編輯密文' ,
viewValueHintJson : '此值會以 JSON 儲存,請輸入合法 JSON。' ,
viewValueInvalidJson : '請輸入合法 JSON 值' ,
viewValueUnlockRequired : '請先在 MCP 設定頁解鎖密碼短語,再修改密文值。' ,
viewSaveChanges : '儲存變更' ,
viewChangesSaved : '已儲存' ,
viewUnlinkConfirm : '確定解除密文關聯「{name}」?' ,
@@ -865,9 +890,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
filterTypeLabel : 'Type' ,
filterTypeAll : 'All' ,
filterSubmit : 'Filter' ,
filterClear : 'Clear ' ,
filterClear : 'Reset ' ,
emptyEntries : 'No entries.' ,
colName : 'Name' ,
colFolder : 'Folder' ,
colType : 'Type' ,
colNotes : 'Notes' ,
colTags : 'Tags' ,
@@ -891,6 +917,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalCancel : 'Cancel' ,
modalSave : 'Save' ,
mobileLabelName : 'Name' ,
mobileLabelFolder : 'Folder' ,
mobileLabelType : 'Type' ,
mobileLabelNotes : 'Notes' ,
mobileLabelTags : 'Tags' ,
@@ -923,6 +950,10 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewCopy : 'Copy' ,
viewCopied : 'Copied' ,
viewLoading : 'Decrypting…' ,
viewEditSecret : 'Edit secret' ,
viewValueHintJson : 'This value is stored as JSON. Enter valid JSON.' ,
viewValueInvalidJson : 'Enter a valid JSON value' ,
viewValueUnlockRequired : 'Unlock your passphrase on the MCP config page before editing secret values.' ,
viewSaveChanges : 'Save changes' ,
viewChangesSaved : 'Saved' ,
viewUnlinkConfirm : 'Unlink secret "{name}"?' ,
@@ -941,6 +972,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
document . querySelectorAll ( 'tr[data-entry-id]' ) . forEach ( function ( tr ) {
var map = {
'.col-name' : 'mobileLabelName' ,
'.col-folder' : 'mobileLabelFolder' ,
'.col-type' : 'mobileLabelType' ,
'.col-notes' : 'mobileLabelNotes' ,
'.col-tags' : 'mobileLabelTags' ,
@@ -956,9 +988,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
rebuildColPanel ( ) ;
} ;
var COL _ORDER = [ 'name' , 'type' , 'notes' , 'tags' , 'relations' , 'secrets' , 'actions' ] ;
var COL _ORDER = [ 'name' , 'folder' , 'type' , 'notes' , 'tags' , 'relations' , 'secrets' , 'actions' ] ;
var COL _ALWAYS _ON = { name : true , actions : true } ;
var COL _DEFAULTS = { name : true , type : true , notes : false , tags : true , relations : true , secrets : false , actions : true } ;
var COL _DEFAULTS = { name : true , folder : true , type : true , notes : false , tags : true , relations : true , secrets : false , actions : true } ;
var COL _STORAGE _KEY = 'entries_col_vis' ;
var colPanel = document . getElementById ( 'col-panel' ) ;
var colToggleBtn = document . getElementById ( 'col-toggle-btn' ) ;
@@ -966,7 +998,16 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
function getColVis ( ) {
try {
var saved = localStorage . getItem ( COL _STORAGE _KEY ) ;
if ( saved ) { var parsed = JSON . parse ( saved ) ; if ( parsed && typeof parsed === 'object' ) return parsed ; }
if ( saved ) {
var parsed = JSON . parse ( saved ) ;
if ( parsed && typeof parsed === 'object' ) {
var merged = { } ;
COL _ORDER . forEach ( function ( col ) {
merged [ col ] = parsed [ col ] !== undefined ? parsed [ col ] : COL _DEFAULTS [ col ] ;
} ) ;
return merged ;
}
}
} catch ( e ) { }
var defaults = { } ;
COL _ORDER . forEach ( function ( col ) { defaults [ col ] = COL _DEFAULTS [ col ] ; } ) ;
@@ -1161,9 +1202,81 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
if ( e . target === viewOverlay ) closeView ( ) ;
} ) ;
function parseEntrySecretSchema ( tr ) {
if ( ! tr ) return [ ] ;
try {
var raw = JSON . parse ( tr . getAttribute ( 'data-entry-secrets' ) || '[]' ) ;
return Array . isArray ( raw ) ? raw : [ ] ;
} catch ( err ) {
return [ ] ;
}
}
function renderEntrySecretChips ( tr , secretSchema ) {
if ( ! tr ) return ;
var list = tr . querySelector ( '.col-secrets .secret-list' ) ;
if ( ! list ) return ;
list . innerHTML = '' ;
( secretSchema || [ ] ) . forEach ( function ( secret ) {
var chip = document . createElement ( 'span' ) ;
chip . className = 'secret-chip' ;
chip . setAttribute ( 'data-secret-id' , secret . id || '' ) ;
var name = document . createElement ( 'span' ) ;
name . className = 'secret-name' ;
name . title = secret . name || '' ;
name . textContent = secret . name || '' ;
var type = document . createElement ( 'span' ) ;
type . className = 'secret-type' ;
type . textContent = secret . secret _type || 'text' ;
chip . appendChild ( name ) ;
chip . appendChild ( type ) ;
list . appendChild ( chip ) ;
} ) ;
var viewBtn = tr . querySelector ( '.btn-view-secrets' ) ;
if ( viewBtn ) viewBtn . disabled = ! ( secretSchema && secretSchema . length ) ;
}
function writeEntrySecretSchema ( entryId , secretSchema ) {
var tr = document . querySelector ( 'tr[data-entry-id="' + entryId + '"]' ) ;
if ( ! tr ) return ;
tr . setAttribute ( 'data-entry-secrets' , JSON . stringify ( secretSchema || [ ] ) ) ;
renderEntrySecretChips ( tr , secretSchema || [ ] ) ;
}
function updateEntrySecretSchema ( entryId , secretId , updater ) {
var tr = document . querySelector ( 'tr[data-entry-id="' + entryId + '"]' ) ;
if ( ! tr ) return ;
var changed = false ;
var next = parseEntrySecretSchema ( tr ) . map ( function ( secret ) {
if ( String ( secret . id || '' ) !== String ( secretId || '' ) ) return secret ;
changed = true ;
return updater ( Object . assign ( { } , secret ) ) ;
} ) ;
if ( changed ) writeEntrySecretSchema ( entryId , next ) ;
}
function removeEntrySecretSchema ( entryId , secretId ) {
var tr = document . querySelector ( 'tr[data-entry-id="' + entryId + '"]' ) ;
if ( ! tr ) return ;
var schema = parseEntrySecretSchema ( tr ) ;
writeEntrySecretSchema ( entryId , schema . filter ( function ( secret ) {
return String ( secret . id || '' ) !== String ( secretId || '' ) ;
} ) ) ;
}
function renderViewSecrets ( secrets , secretSchema ) {
viewBody . innerHTML = '' ;
var names = Object . keys ( secrets ) ;
var names = [ ] ;
( secretSchema || [ ] ) . forEach ( function ( secret ) {
if ( Object . prototype . hasOwnProperty . call ( secrets , secret . name ) ) names . push ( secret . name ) ;
} ) ;
Object . keys ( secrets ) . forEach ( function ( name ) {
if ( names . indexOf ( name ) === - 1 ) names . push ( name ) ;
} ) ;
if ( names . length === 0 ) {
var msg = document . createElement ( 'div' ) ;
msg . className = 'view-locked-msg' ;
@@ -1177,18 +1290,42 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
names . forEach ( function ( name ) {
var raw = secrets [ name ] ;
var valueStr = ( raw === null || raw === undefined ) ? '' :
( typeof raw === 'object ' ) ? JSON . stringify ( raw , null , 2 ) : String ( raw ) ;
var currentName = name ;
var valueMode = ( typeof raw === 'string ' ) ? 'text' : 'json' ;
var valueStr = ( typeof raw === 'string' ) ? raw : JSON . stringify ( raw , null , 2 ) ;
var schema = schemaMap [ name ] || { } ;
var secretId = schema . id || '' ;
var sec retType = schema . secret _type || 'text' ;
var originalName = name ;
var hasChanges = f als e;
var cur ren tType = schema . secret _type || 'text' ;
function formatSecretValue ( v alu e) {
return ( typeof value === 'string' ) ? value : JSON . stringify ( value , null , 2 ) ;
}
function parseEditedSecretValue ( text ) {
if ( valueMode === 'text' ) return { ok : true , value : text } ;
try {
return { ok : true , value : JSON . parse ( text ) } ;
} catch ( err ) {
return { ok : false , error : t ( 'viewValueInvalidJson' ) } ;
}
}
function comparableSecretValue ( value ) {
return JSON . stringify ( value ) ;
}
function applyCurrentSecretValue ( value ) {
raw = value ;
valueStr = formatSecretValue ( value ) ;
valueEl . textContent = valueStr ;
valueEditor . value = valueStr ;
}
var row = document . createElement ( 'div' ) ;
row . className = 'view-secret-row' ;
row . setAttribute ( 'data-secret-id' , secretId ) ;
row . setAttribute ( 'data-original-name' , original Name) ;
row . setAttribute ( 'data-original-name' , current Name) ;
var header = document . createElement ( 'div' ) ;
header . className = 'view-secret-header' ;
@@ -1203,14 +1340,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var nameInput = document . createElement ( 'input' ) ;
nameInput . type = 'text' ;
nameInput . className = 'view-secret-name-input' ;
nameInput . value = n ame;
nameInput . value = currentN ame;
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 = sec retType;
typeBadge . textContent = cur ren tType;
var typeSelect = document . createElement ( 'select' ) ;
typeSelect . className = 'view-secret-type-select' ;
@@ -1219,13 +1355,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var option = document . createElement ( 'option' ) ;
option . value = opt ;
option . textContent = opt ;
if ( opt === sec retType) option . selected = true ;
if ( opt === cur ren tType) option . selected = true ;
typeSelect . appendChild ( option ) ;
} ) ;
if ( SECRET _TYPE _OPTIONS . indexOf ( sec retType) === - 1 && sec retType) {
if ( SECRET _TYPE _OPTIONS . indexOf ( cur ren tType) === - 1 && cur ren tType) {
var fallback = document . createElement ( 'option' ) ;
fallback . value = sec retType;
fallback . textContent = sec retType;
fallback . value = cur ren tType;
fallback . textContent = cur ren tType;
fallback . selected = true ;
typeSelect . appendChild ( fallback ) ;
}
@@ -1242,8 +1378,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var editBtn = document . createElement ( 'button' ) ;
editBtn . type = 'button' ;
editBtn . className = 'btn-icon btn-view-edit' ;
editBtn . textContent = '✎' ;
editBtn . title = t ( 'renameSecretTitle' ) ;
editBtn . textContent = t ( 'viewEditSecret' ) ;
var saveBtn = document . createElement ( 'button' ) ;
saveBtn . type = 'button' ;
@@ -1272,8 +1407,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var unlinkBtn = document . createElement ( 'button' ) ;
unlinkBtn . type = 'button' ;
unlinkBtn . className = 'btn-icon btn-view-unlink' ;
unlinkBtn . textContent = '× ' ;
unlinkBtn . title = t ( 'unlinkTitle' ) ;
unlinkBtn . textContent = t ( 'unlinkTitle' ) ;
actions . appendChild ( unlinkBtn ) ;
actions . appendChild ( editBtn ) ;
@@ -1287,9 +1421,23 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var valueEl = document . createElement ( 'div' ) ;
valueEl . className = 'view-secret-value' ;
valueEl . textContent = valueStr ;
var valueEditor = document . createElement ( 'textarea' ) ;
valueEditor . className = 'view-secret-editor' ;
valueEditor . hidden = true ;
valueEditor . value = valueStr ;
valueWrap . appendChild ( valueEl ) ;
valueWrap . appendChild ( valueEditor ) ;
row . appendChild ( valueWrap ) ;
var valueHint = null ;
if ( valueMode === 'json' ) {
valueHint = document . createElement ( 'div' ) ;
valueHint . className = 'view-secret-hint' ;
valueHint . hidden = true ;
valueHint . textContent = t ( 'viewValueHintJson' ) ;
row . appendChild ( valueHint ) ;
}
var nameStatus = document . createElement ( 'div' ) ;
nameStatus . className = 'secret-name-status' ;
nameStatus . setAttribute ( 'data-status' , 'idle' ) ;
@@ -1301,8 +1449,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
function enterEditMode ( ) {
nameSpan . hidden = true ;
typeBadge . hidden = true ;
valueEl . hidden = true ;
nameInput . hidden = false ;
typeSelect . hidden = false ;
valueEditor . hidden = false ;
if ( valueHint ) valueHint . hidden = false ;
saveBtn . hidden = false ;
cancelBtn . hidden = false ;
editBtn . hidden = true ;
@@ -1313,8 +1464,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
function exitEditMode ( ) {
nameSpan . hidden = false ;
typeBadge . hidden = false ;
valueEl . hidden = false ;
nameInput . hidden = true ;
typeSelect . hidden = true ;
valueEditor . hidden = true ;
if ( valueHint ) valueHint . hidden = true ;
saveBtn . hidden = true ;
cancelBtn . hidden = true ;
editBtn . hidden = false ;
@@ -1322,7 +1476,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
nameStatus . className = 'secret-name-status' ;
nameInput . value = nameSpan . textContent ;
typeSelect . value = typeBadge . textContent ;
hasChanges = false ;
valueEditor . value = valueStr ;
}
editBtn . addEventListener ( 'click' , enterEditMode ) ;
@@ -1337,7 +1491,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
nameStatus . className = 'secret-name-status' ;
debounceTimer = setTimeout ( function ( ) {
var newName = nameInput . value . trim ( ) ;
if ( ! newName || newName === original Name) return ;
if ( ! newName || newName === current Name) return ;
nameStatus . textContent = t ( 'checkingSecretName' ) ;
nameStatus . className = 'secret-name-status checking' ;
var checkId = Date . now ( ) ;
@@ -1352,18 +1506,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
if ( data . ok && data . available ) {
nameStatus . textContent = t ( 'secretNameAvailable' ) ;
nameStatus . className = 'secret-name-status success' ;
hasChanges = true ;
} else {
nameStatus . textContent = data . error || t ( 'secretNameTaken' ) ;
nameStatus . className = 'secret-name-status error' ;
hasChanges = false ;
}
} )
. catch ( function ( ) {
if ( currentCheck !== checkId ) return ;
nameStatus . textContent = t ( 'secretNameCheckError' ) ;
nameStatus . className = 'secret-name-status error' ;
hasChanges = false ;
} ) ;
} , 300 ) ;
} ) ;
@@ -1372,21 +1523,46 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
if ( e . key === 'Enter' ) { e . preventDefault ( ) ; saveBtn . click ( ) ; }
if ( e . key === 'Escape' ) { cancelBtn . click ( ) ; }
} ) ;
valueEditor . addEventListener ( 'keydown' , function ( e ) {
if ( ( e . metaKey || e . ctrlKey ) && e . key === 'Enter' ) {
e . preventDefault ( ) ;
saveBtn . click ( ) ;
}
if ( e . key === 'Escape' ) cancelBtn . click ( ) ;
} ) ;
// ── Save ──
saveBtn . addEventListener ( 'click' , function ( ) {
var newName = nameInput . value . trim ( ) ;
var newType = typeSelect . value ;
var parsedValue = parseEditedSecretValue ( valueEditor . value ) ;
if ( ! newName ) { nameStatus . textContent = t ( 'secretNameInvalid' ) ; nameStatus . className = 'secret-name-status error' ; return ; }
if ( ! newType ) { nameStatus . textContent = t ( 'secretTypeInvalid' ) ; nameStatus . className = 'secret-name-status error' ; return ; }
if ( ! parsedValue . ok ) { nameStatus . textContent = parsedValue . error ; nameStatus . className = 'secret-name-status error' ; return ; }
var nextValue = parsedValue . value ;
var patchBody = { } ;
if ( newName != = originalName ) patchBody . name = newName ;
if ( newTyp e !== secretTyp e) patchBody . typ e = newTyp e ;
var valueChanged = comparableSecretValue ( nextValue ) !== comparableSecretValue ( raw ) ;
if ( newNam e !== currentNam e) patchBody . nam e = newNam e ;
if ( newType !== currentType ) patchBody . type = newType ;
if ( valueChanged ) patchBody . value = nextValue ;
if ( Object . keys ( patchBody ) . length === 0 ) { exitEditMode ( ) ; return ; }
var encKey = sessionStorage . getItem ( 'enc_key' ) ;
if ( valueChanged && ! encKey ) {
nameStatus . textContent = t ( 'viewValueUnlockRequired' ) ;
nameStatus . className = 'secret-name-status error' ;
return ;
}
saveBtn . textContent = '...' ;
saveBtn . disabled = true ;
cancelBtn . disabled = true ;
editBtn . disabled = true ;
fetch ( '/api/secrets/' + encodeURIComponent ( secretId ) , {
method : 'PATCH' ,
headers : { 'Content-Type' : 'application/json' } ,
headers : Object . assign (
{ 'Content-Type' : 'application/json' } ,
valueChanged ? { 'X-Encryption-Key' : encKey } : { }
) ,
credentials : 'same-origin' ,
body : JSON . stringify ( patchBody )
} ) . then ( function ( r ) {
@@ -1395,22 +1571,28 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
return data ;
} ) ;
} ) . then ( function ( ) {
currentName = newName ;
currentType = newType ;
nameSpan . textContent = newName ;
typeBadge . textContent = newType ;
originalName = newName ;
nameInput . setAttribute ( 'data-original-name' , newName ) ;
applyCurrentSecretValue ( nextValue ) ;
saveBtn . textContent = t ( 'viewChangesSaved' ) ;
nameStatus . textContent = t ( 'viewChangesSaved' ) ;
nameStatus . className = 'secret-name-status success' ;
updateEntrySecretSchema ( viewBody . getAttribute ( 'data-entry-id' ) , secretId , function ( secret ) {
secret . name = newName ;
secret . secret _type = newType ;
return secret ;
} ) ;
setTimeout ( function ( ) { exitEditMode ( ) ; saveBtn . textContent = t ( 'viewSaveChanges' ) ; } , 1200 ) ;
// Update table row chip
var tableRow = document . querySelector ( 'tr[data-entry-id="' + viewBody . getAttribute ( 'data-entry-id' ) + '"]' ) ;
if ( tableRow ) {
var chip = tableRow . querySelector ( '.secret-chip .secret-name' ) ;
if ( chip && chip . textContent === name ) chip . textContent = newName ;
}
} ) . catch ( function ( err ) {
nameStatus . textContent = err . message || String ( err ) ;
nameStatus . className = 'secret-name-status error' ;
saveBtn . textContent = t ( 'viewSaveChanges' ) ;
} ) . finally ( function ( ) {
saveBtn . disabled = false ;
cancelBtn . disabled = false ;
editBtn . disabled = false ;
} ) ;
} ) ;
@@ -1434,15 +1616,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
msg . textContent = t ( 'viewNoSecrets' ) ;
viewBody . appendChild ( msg ) ;
}
// Update table row
var tableRow = document . querySelector ( 'tr[data-entry-id="' + viewBody . getAttribute ( 'data-entry-id' ) + '"]' ) ;
if ( tableRow ) {
var chip = tableRow . querySelector ( '.secret-chip' ) ;
if ( chip ) {
var chipName = chip . querySelector ( '.secret-name' ) ;
if ( chipName && chipName . textContent === name ) chip . remove ( ) ;
}
}
removeEntrySecretSchema ( viewBody . getAttribute ( 'data-entry-id' ) , secretId ) ;
} ) . catch ( function ( err ) {
alert ( err . message || String ( err ) ) ;
} ) ;