@@ -21,7 +21,7 @@ import { t } from 'perry/i18n';
2121import { MongoClient } from '@perryts/mongodb' ;
2222import { bsonStringify , bsonParse } from './data/bson-json' ;
2323import { HoneCodeEditorWidget } from '@honeide/editor/perry' ;
24- import { getAllConnections , createConnection , deleteConnection , saveState , getState , setWebTransient } from './data/connection-store' ;
24+ import { getAllConnections , createConnection , updateConnection , deleteConnection , saveState , getState , setWebTransient } from './data/connection-store' ;
2525import { trackAppLaunch , trackConnect , trackQuery } from './data/telemetry' ;
2626import { parallelMap , spawn } from 'perry/thread' ;
2727import { prettyPrintJson , extractIdShort , extractFields , processDocForDisplay } from './data/json-utils' ;
@@ -148,6 +148,9 @@ let formPort = '27017';
148148let formUser = '' ;
149149let formPass = '' ;
150150let formUri = '' ;
151+ // Empty string = creating a new connection. Non-empty = editing the existing
152+ // row with this id; Save updates instead of inserts.
153+ let editingConnId = '' ;
151154
152155let currentDbName = '' ;
153156let currentCollName = '' ;
@@ -646,14 +649,18 @@ function refreshConnectionList(): void {
646649 widgetSetHidden ( confirmNo , 0 ) ;
647650 } ) ;
648651
652+ const editBtn = Button ( t ( 'Edit' ) , ( ) => { showConnectionForm ( connId ) ; } ) ;
653+ buttonSetBordered ( editBtn , 0 ) ;
654+ buttonSetTextColor ( editBtn , tsR , tsG , tsB , 1.0 ) ;
655+
649656 let card : any ;
650657 if ( mobile ) {
651658 // Mobile: stack vertically — info on top, buttons below
652- const btnRow = HStack ( 8 , [ deleteBtn , Spacer ( ) , connectBtn ] ) ;
659+ const btnRow = HStack ( 8 , [ editBtn , deleteBtn , Spacer ( ) , connectBtn ] ) ;
653660 const confirmRow = HStack ( 8 , [ confirmLabel , confirmYes , confirmNo ] ) ;
654661 card = VStack ( 10 , [ info , confirmRow , btnRow ] ) ;
655662 } else {
656- const row = HStack ( 12 , [ accentBar , info , Spacer ( ) , confirmLabel , confirmYes , confirmNo , deleteBtn , connectBtn ] ) ;
663+ const row = HStack ( 12 , [ accentBar , info , Spacer ( ) , confirmLabel , confirmYes , confirmNo , editBtn , deleteBtn , connectBtn ] ) ;
657664 card = VStack ( 0 , [ row ] ) ;
658665 }
659666 widgetSetBackgroundColor ( card , sfR , sfG , sfB , 1.0 ) ;
@@ -677,17 +684,86 @@ function refreshConnectionList(): void {
677684const formContainer = VStack ( 12 , [ ] ) ;
678685widgetSetHidden ( formContainer , 1 ) ;
679686
680- function showConnectionForm ( ) : void {
687+ // Build (uri, displayHost, portNum, name) from the live form fields.
688+ // Shared by Save and Test so both see the exact same values the user
689+ // entered (including unsubmitted edits to TextFields).
690+ function readConnectionForm (
691+ nameField : any , hostField : any , portField : any ,
692+ userField : any , passField : any , uriField : any
693+ ) : { uri : string ; name : string ; displayHost : string ; portNum : number } {
694+ const nameRaw = textfieldGetString ( nameField ) ;
695+ const name = ( typeof nameRaw === 'string' && nameRaw . length > 0 ) ? nameRaw : ( formName || t ( 'Untitled' ) ) ;
696+ const hostRaw = textfieldGetString ( hostField ) ;
697+ const host = ( typeof hostRaw === 'string' && hostRaw . length > 0 ) ? hostRaw : ( formHost || 'localhost' ) ;
698+ const portRaw = textfieldGetString ( portField ) ;
699+ const port = ( typeof portRaw === 'string' && portRaw . length > 0 ) ? portRaw : ( formPort || '27017' ) ;
700+ const userStr = textfieldGetString ( userField ) + '' ;
701+ const passStr = textfieldGetString ( passField ) + '' ;
702+ const uriStr = textfieldGetString ( uriField ) + '' ;
703+
704+ let uri = '' ;
705+ if ( uriStr . length > 0 ) {
706+ uri = uriStr ;
707+ } else if ( userStr . length > 0 && passStr . length > 0 ) {
708+ uri = 'mongodb://' + userStr + ':' + passStr + '@' + host + ':' + port ;
709+ } else {
710+ uri = 'mongodb://' + host + ':' + port ;
711+ }
712+
713+ let displayHost = host ;
714+ let displayPort = port ;
715+ const schemeIdx = uri . indexOf ( '://' ) ;
716+ if ( schemeIdx >= 0 ) {
717+ const afterScheme = uri . substring ( schemeIdx + 3 ) ;
718+ const atIdx = afterScheme . indexOf ( '@' ) ;
719+ const hostPart = atIdx >= 0 ? afterScheme . substring ( atIdx + 1 ) : afterScheme ;
720+ const slashIdx = hostPart . indexOf ( '/' ) ;
721+ const hostPortStr = slashIdx >= 0 ? hostPart . substring ( 0 , slashIdx ) : hostPart ;
722+ const colonIdx = hostPortStr . lastIndexOf ( ':' ) ;
723+ if ( colonIdx >= 0 ) {
724+ displayHost = hostPortStr . substring ( 0 , colonIdx ) ;
725+ displayPort = hostPortStr . substring ( colonIdx + 1 ) ;
726+ } else {
727+ displayHost = hostPortStr ;
728+ }
729+ }
730+
731+ let portNum = 27017 ;
732+ if ( displayPort . length > 0 ) {
733+ const p = Number ( displayPort ) ;
734+ if ( p > 0 ) portNum = p ;
735+ }
736+
737+ return { uri : uri , name : name , displayHost : displayHost , portNum : portNum } ;
738+ }
739+
740+ // Find a profile by id from the loaded list. Returns null when not found.
741+ function lookupConnection ( id : string ) : any {
742+ if ( ! id || id . length === 0 ) return null ;
743+ const all : any = getAllConnections ( ) ;
744+ for ( let i = 0 ; i < all . length ; i ++ ) {
745+ const p : any = all [ i ] ;
746+ if ( p && p . id === id ) return p ;
747+ }
748+ return null ;
749+ }
750+
751+ function showConnectionForm ( editId ?: string ) : void {
681752 widgetClearChildren ( formContainer ) ;
682753 widgetSetHidden ( connListContainer , 1 ) ;
683754 widgetSetHidden ( formContainer , 0 ) ;
684755
756+ // Resolve edit target (if any). Empty string = new-connection mode.
757+ const wantEdit : string = ( typeof editId === 'string' && editId . length > 0 ) ? editId : '' ;
758+ const existing : any = wantEdit . length > 0 ? lookupConnection ( wantEdit ) : null ;
759+ editingConnId = existing ? existing . id : '' ;
760+
685761 const formCard = VStack ( 12 , [ ] ) ;
686762 widgetSetBackgroundColor ( formCard , sfR , sfG , sfB , 1.0 ) ;
687763 setCornerRadius ( formCard , 12 ) ;
688764 setPadding ( formCard , 20 , 24 , 20 , 24 ) ;
689765
690- const title = makeLabel ( ' New Connection', 18 , true ) ;
766+ const title = makeLabel ( existing ? t ( 'Edit Connection' ) : t ( ' New Connection') , 18 , true ) ;
691767
692768 const nameLabel = makeSecondary ( t ( 'Name' ) , 11 ) ;
693769 const nameField = TextField ( 'e.g. Production, Local dev...' , ( val : string ) => { formName = val ; } ) ;
@@ -709,70 +785,77 @@ function showConnectionForm(): void {
709785 const uriLabel = makeSecondary ( t ( 'Connection String' ) , 11 ) ;
710786 const uriField = TextField ( 'mongodb+srv://user:pass@cluster.example.com/db' , ( val : string ) => { formUri = val ; } ) ;
711787
788+ // Prefill in edit mode. Only the URI field is restored when present —
789+ // host/port/name come from the stored row. Username/password aren't
790+ // prefilled because they're embedded inside the URI.
791+ if ( existing ) {
792+ textfieldSetString ( nameField , existing . name || '' ) ;
793+ textfieldSetString ( hostField , existing . host || 'localhost' ) ;
794+ textfieldSetString ( portField , String ( existing . port || 27017 ) ) ;
795+ textfieldSetString ( uriField , existing . connectionString || '' ) ;
796+ formName = existing . name || '' ;
797+ formHost = existing . host || 'localhost' ;
798+ formPort = String ( existing . port || 27017 ) ;
799+ formUri = existing . connectionString || '' ;
800+ }
801+
712802 // Tab key navigation: name → host → port → user → pass → uri
713803 textfieldSetNextKeyView ( nameField , hostField ) ;
714804 textfieldSetNextKeyView ( hostField , portField ) ;
715805 textfieldSetNextKeyView ( portField , userField ) ;
716806 textfieldSetNextKeyView ( userField , passField ) ;
717807 textfieldSetNextKeyView ( passField , uriField ) ;
718808
719- const saveBtn = Button ( 'Save Connection' , ( ) => {
720- try {
721- const nameRaw = textfieldGetString ( nameField ) ;
722- const name = ( typeof nameRaw === 'string' && nameRaw . length > 0 ) ? nameRaw : ( formName || t ( 'Untitled' ) ) ;
723- const hostRaw = textfieldGetString ( hostField ) ;
724- const host = ( typeof hostRaw === 'string' && hostRaw . length > 0 ) ? hostRaw : ( formHost || 'localhost' ) ;
725- const portRaw = textfieldGetString ( portField ) ;
726- const port = ( typeof portRaw === 'string' && portRaw . length > 0 ) ? portRaw : ( formPort || '27017' ) ;
727- // Build URI using string concatenation (+) which Perry's codegen handles
728- // correctly for is_string locals. encodeURIComponent and || fail due to
729- // NaN-boxing being stripped (see PerryTS/perry#10, #12).
730- // Read user/pass into string variables via + '' (forces string concat path)
731- const userStr = textfieldGetString ( userField ) + '' ;
732- const passStr = textfieldGetString ( passField ) + '' ;
733- const uriStr = textfieldGetString ( uriField ) + '' ;
734-
735- let uri = '' ;
736- if ( uriStr . length > 0 ) {
737- uri = uriStr ;
738- } else if ( userStr . length > 0 && passStr . length > 0 ) {
739- // Note: not using encodeURIComponent — it corrupts NaN-boxed strings.
740- // Users with special chars in user/pass should use the URI field instead.
741- uri = 'mongodb://' + userStr + ':' + passStr + '@' + host + ':' + port ;
742- } else {
743- uri = 'mongodb://' + host + ':' + port ;
744- }
745-
746- // Extract host (without port) from URI for display in connection list
747- let displayHost = host ;
748- let displayPort = port ;
749- const schemeIdx = uri . indexOf ( '://' ) ;
750- if ( schemeIdx >= 0 ) {
751- const afterScheme = uri . substring ( schemeIdx + 3 ) ;
752- const atIdx = afterScheme . indexOf ( '@' ) ;
753- const hostPart = atIdx >= 0 ? afterScheme . substring ( atIdx + 1 ) : afterScheme ;
754- const slashIdx = hostPart . indexOf ( '/' ) ;
755- const hostPortStr = slashIdx >= 0 ? hostPart . substring ( 0 , slashIdx ) : hostPart ;
756- const colonIdx = hostPortStr . lastIndexOf ( ':' ) ;
757- if ( colonIdx >= 0 ) {
758- displayHost = hostPortStr . substring ( 0 , colonIdx ) ;
759- displayPort = hostPortStr . substring ( colonIdx + 1 ) ;
760- } else {
761- displayHost = hostPortStr ;
762- }
809+ // Test-result label: shown by the Test button to report success/failure
810+ // inline without saving. Hidden until first Test press.
811+ const testStatus = Text ( '' ) ;
812+ textSetFontSize ( testStatus , 12 ) ;
813+ textSetFontFamily ( testStatus , uiFont ) ;
814+ textSetWraps ( testStatus , 0 ) ;
815+ widgetSetHidden ( testStatus , 1 ) ;
816+
817+ const testBtn = Button ( t ( 'Test Connection' ) , async ( ) => {
818+ const f = readConnectionForm ( nameField , hostField , portField , userField , passField , uriField ) ;
819+ textSetString ( testStatus , t ( 'Testing...' ) ) ;
820+ textSetColor ( testStatus , tmR , tmG , tmB , 1.0 ) ;
821+ widgetSetHidden ( testStatus , 0 ) ;
822+ const ok = await connectToMongo ( f . uri ) ;
823+ if ( ok ) {
824+ // Don't keep the test connection live — drop it so Save/Connect
825+ // later opens a fresh client. Best-effort close, ignore errors.
826+ if ( mongoClient ) {
827+ try { await mongoClient . close ( ) ; } catch ( e : any ) { }
828+ mongoClient = null ;
829+ currentConnUri = '' ;
763830 }
831+ textSetString ( testStatus , t ( 'Connection OK' ) ) ;
832+ textSetColor ( testStatus , 0.13 , 0.55 , 0.13 , 1.0 ) ; // green
833+ } else {
834+ textSetString ( testStatus , t ( 'Failed' ) + ': ' + lastConnError ) ;
835+ textSetColor ( testStatus , erR , erG , erB , 1.0 ) ;
836+ }
837+ } ) ;
838+ buttonSetBordered ( testBtn , 0 ) ;
839+ buttonSetTextColor ( testBtn , moR , moG , moB , 1.0 ) ;
764840
765- // Parse port number (avoid parseInt which may not work in Perry AOT)
766- let portNum = 27017 ;
767- if ( displayPort . length > 0 ) {
768- const p = Number ( displayPort ) ;
769- if ( p > 0 ) portNum = p ;
841+ const saveBtn = Button ( existing ? t ( 'Update Connection' ) : t ( 'Save Connection' ) , ( ) => {
842+ try {
843+ const f = readConnectionForm ( nameField , hostField , portField , userField , passField , uriField ) ;
844+
845+ let savedId : string ;
846+ if ( editingConnId . length > 0 ) {
847+ updateConnection ( editingConnId , {
848+ name : f . name , host : f . displayHost , port : f . portNum , connectionString : f . uri ,
849+ } ) ;
850+ savedId = editingConnId ;
851+ } else {
852+ const profile : any = createConnection ( {
853+ name : f . name , host : f . displayHost , port : f . portNum , connectionString : f . uri ,
854+ } ) ;
855+ savedId = profile . id ;
770856 }
771-
772- // Create the connection profile in SQLite
773- const profile : any = createConnection ( { name : name , host : displayHost , port : portNum , connectionString : uri } ) ;
774- // Also try Keychain as backup
775- if ( ! isWeb ) keychainSave ( 'mango-conn-' + profile . id , uri ) ;
857+ // Keychain backup of the URI (native only).
858+ if ( ! isWeb ) keychainSave ( 'mango-conn-' + savedId , f . uri ) ;
776859
777860 formName = '' ;
778861 formHost = 'localhost' ;
@@ -782,6 +865,7 @@ function showConnectionForm(): void {
782865 saveState ( '_fu' , '' ) ;
783866 saveState ( '_fp' , '' ) ;
784867 formUri = '' ;
868+ editingConnId = '' ;
785869 widgetSetHidden ( formContainer , 1 ) ;
786870 widgetSetHidden ( connListContainer , 0 ) ;
787871 loadConnections ( ) ;
@@ -794,6 +878,7 @@ function showConnectionForm(): void {
794878 buttonSetTextColor ( saveBtn , moR , moG , moB , 1.0 ) ;
795879
796880 const cancelBtn = makeGhostBtn ( t ( 'Cancel' ) , ( ) => {
881+ editingConnId = '' ;
797882 widgetSetHidden ( formContainer , 1 ) ;
798883 widgetSetHidden ( connListContainer , 0 ) ;
799884 } ) ;
@@ -813,7 +898,8 @@ function showConnectionForm(): void {
813898 widgetAddChild ( formCard , divLabel ) ;
814899 widgetAddChild ( formCard , uriLabel ) ;
815900 widgetAddChild ( formCard , uriField ) ;
816- widgetAddChild ( formCard , HStack ( 8 , [ cancelBtn , Spacer ( ) , saveBtn ] ) ) ;
901+ widgetAddChild ( formCard , testStatus ) ;
902+ widgetAddChild ( formCard , HStack ( 8 , [ cancelBtn , Spacer ( ) , testBtn , saveBtn ] ) ) ;
817903
818904 widgetAddChild ( formContainer , formCard ) ;
819905 widgetMatchParentWidth ( formCard ) ;
0 commit comments