@@ -15,6 +15,7 @@ import Option from 'components/Dropdown/Option.react';
1515import Toolbar from 'components/Toolbar/Toolbar.react' ;
1616import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react' ;
1717import Notification from 'dashboard/Data/Browser/Notification.react' ;
18+ import Modal from 'components/Modal/Modal.react' ;
1819import * as ColumnPreferences from 'lib/ColumnPreferences' ;
1920import * as ClassPreferences from 'lib/ClassPreferences' ;
2021import ViewPreferencesManager from 'lib/ViewPreferencesManager' ;
@@ -47,6 +48,8 @@ export default class DashboardSettings extends DashboardView {
4748 passwordHidden : true ,
4849 migrationLoading : false ,
4950 storagePreference : 'local' , // Will be updated in componentDidMount
51+ showConflictModal : false ,
52+ migrationConflicts : [ ] ,
5053 copyData : {
5154 data : '' ,
5255 show : false ,
@@ -95,7 +98,7 @@ export default class DashboardSettings extends DashboardView {
9598 }
9699 }
97100
98- async migrateToServer ( ) {
101+ async migrateToServer ( overwriteConflicts = false ) {
99102 if ( ! this . viewPreferencesManager ) {
100103 this . showNote ( 'ViewPreferencesManager not initialized' ) ;
101104 return ;
@@ -106,6 +109,11 @@ export default class DashboardSettings extends DashboardView {
106109 return ;
107110 }
108111
112+ if ( ! this . scriptManager ) {
113+ this . showNote ( 'ScriptManager not initialized' ) ;
114+ return ;
115+ }
116+
109117 if ( ! this . viewPreferencesManager . isServerConfigEnabled ( ) ) {
110118 this . showNote ( 'Server configuration is not enabled for this app. Please add a "config" section to your app configuration.' ) ;
111119 return ;
@@ -115,14 +123,30 @@ export default class DashboardSettings extends DashboardView {
115123
116124 try {
117125 // Migrate views
118- const viewsResult = await this . viewPreferencesManager . migrateToServer ( this . context . applicationId ) ;
126+ const viewsResult = await this . viewPreferencesManager . migrateToServer ( this . context . applicationId , overwriteConflicts ) ;
119127
120128 // Migrate filters
121- const filtersResult = await this . filterPreferencesManager . migrateToServer ( this . context . applicationId ) ;
129+ const filtersResult = await this . filterPreferencesManager . migrateToServer ( this . context . applicationId , overwriteConflicts ) ;
130+
131+ // Migrate scripts
132+ const scriptsResult = await this . scriptManager . migrateToServer ( this . context . applicationId , overwriteConflicts ) ;
133+
134+ // Check for conflicts
135+ const allConflicts = [
136+ ...( viewsResult . conflicts || [ ] ) ,
137+ ...( filtersResult . conflicts || [ ] ) ,
138+ ...( scriptsResult . conflicts || [ ] )
139+ ] ;
140+
141+ if ( allConflicts . length > 0 && ! overwriteConflicts ) {
142+ // Show conflict dialog
143+ this . showConflictDialog ( allConflicts ) ;
144+ return ;
145+ }
122146
123- const totalItems = viewsResult . viewCount + filtersResult . filterCount ;
147+ const totalItems = viewsResult . viewCount + filtersResult . filterCount + scriptsResult . scriptCount ;
124148
125- if ( viewsResult . success && filtersResult . success ) {
149+ if ( viewsResult . success && filtersResult . success && scriptsResult . success ) {
126150 if ( totalItems > 0 ) {
127151 const messages = [ ] ;
128152 if ( viewsResult . viewCount > 0 ) {
@@ -131,9 +155,12 @@ export default class DashboardSettings extends DashboardView {
131155 if ( filtersResult . filterCount > 0 ) {
132156 messages . push ( `${ filtersResult . filterCount } filter(s)` ) ;
133157 }
134- this . showNote ( `Successfully migrated ${ messages . join ( ' and ' ) } to server storage.` ) ;
158+ if ( scriptsResult . scriptCount > 0 ) {
159+ messages . push ( `${ scriptsResult . scriptCount } script(s)` ) ;
160+ }
161+ this . showNote ( `Successfully migrated ${ messages . join ( ', ' ) } to server storage.` ) ;
135162 } else {
136- this . showNote ( 'No views or filters found to migrate.' ) ;
163+ this . showNote ( 'No views, filters, or scripts found to migrate.' ) ;
137164 }
138165 }
139166 } catch ( error ) {
@@ -143,6 +170,25 @@ export default class DashboardSettings extends DashboardView {
143170 }
144171 }
145172
173+ showConflictDialog ( conflicts ) {
174+ this . setState ( {
175+ migrationLoading : false ,
176+ showConflictModal : true ,
177+ migrationConflicts : conflicts
178+ } ) ;
179+ }
180+
181+ handleConflictOverwrite ( ) {
182+ this . setState ( { showConflictModal : false } ) ;
183+ // User chose to overwrite - retry migration with overwrite flag
184+ this . migrateToServer ( true ) ;
185+ }
186+
187+ handleConflictCancel ( ) {
188+ this . setState ( { showConflictModal : false , migrationConflicts : [ ] } ) ;
189+ this . showNote ( 'Migration aborted. Server settings were not modified.' ) ;
190+ }
191+
146192 async deleteFromBrowser ( ) {
147193 if ( ! window . confirm ( 'Are you sure you want to delete all dashboard settings from browser storage? This action cannot be undone.' ) ) {
148194 return ;
@@ -533,7 +579,7 @@ export default class DashboardSettings extends DashboardView {
533579 label = {
534580 < Label
535581 text = "Migrate Settings to Server"
536- description = "Migrates browser-stored settings to the server. ⚠️ This overwrites existing dashboard settings on the server ."
582+ description = "Migrates browser-stored settings to the server. If conflicts are detected, you'll be asked whether to overwrite or abort ."
537583 />
538584 }
539585 input = {
@@ -565,12 +611,119 @@ export default class DashboardSettings extends DashboardView {
565611 { this . state . copyData . show && copyData }
566612 { this . state . createUserInput && createUserInput }
567613 { this . state . newUser . show && userData }
614+ { this . state . showConflictModal && this . renderConflictModal ( ) }
568615 < Toolbar section = "Settings" subsection = "Dashboard Configuration" />
569616 < Notification note = { this . state . message } isErrorNote = { false } />
570617 </ div >
571618 ) ;
572619 }
573620
621+ renderConflictModal ( ) {
622+ const { migrationConflicts } = this . state ;
623+
624+ // Format conflict details
625+ const viewConflicts = migrationConflicts . filter ( c => c . type === 'view' ) ;
626+ const filterConflicts = migrationConflicts . filter ( c => c . type === 'filter' ) ;
627+ const scriptConflicts = migrationConflicts . filter ( c => c . type === 'script' ) ;
628+
629+ const conflictList = (
630+ < div style = { { padding : '20px' , fontSize : '14px' } } >
631+ < p style = { { marginBottom : '15px' } } >
632+ The following settings already exist on the server:
633+ </ p >
634+
635+ < div style = { {
636+ maxHeight : '300px' ,
637+ overflowY : 'auto' ,
638+ marginBottom : '15px' ,
639+ border : '1px solid #e0e0e0' ,
640+ borderRadius : '4px' ,
641+ padding : '10px'
642+ } } >
643+ { viewConflicts . length > 0 && (
644+ < div style = { { marginBottom : ( filterConflicts . length > 0 || scriptConflicts . length > 0 ) ? '15px' : '0' } } >
645+ < strong > Views ({ viewConflicts . length } ):</ strong >
646+ < ul style = { { marginTop : '5px' , marginBottom : '0' , paddingLeft : '20px' } } >
647+ { viewConflicts . map ( conflict => {
648+ const viewName = conflict . local ?. name || conflict . server ?. name || '' ;
649+ return (
650+ < li key = { conflict . id } >
651+ { viewName || 'Unnamed view' }
652+ < span style = { { fontFamily : 'monospace' , fontSize : '12px' , color : '#666' , marginLeft : '8px' } } >
653+ [{ conflict . id } ]
654+ </ span >
655+ </ li >
656+ ) ;
657+ } ) }
658+ </ ul >
659+ </ div >
660+ ) }
661+
662+ { filterConflicts . length > 0 && (
663+ < div style = { { marginBottom : scriptConflicts . length > 0 ? '15px' : '0' } } >
664+ < strong > Filters ({ filterConflicts . length } ):</ strong >
665+ < ul style = { { marginTop : '5px' , marginBottom : '0' , paddingLeft : '20px' } } >
666+ { filterConflicts . map ( conflict => {
667+ const filterName = conflict . local ?. name || conflict . server ?. name || '' ;
668+ const className = conflict . className || 'Unknown class' ;
669+ const displayText = filterName
670+ ? `${ filterName } (${ className } )`
671+ : className ;
672+ return (
673+ < li key = { conflict . id } >
674+ { displayText }
675+ < span style = { { fontFamily : 'monospace' , fontSize : '12px' , color : '#666' , marginLeft : '8px' } } >
676+ [{ conflict . id } ]
677+ </ span >
678+ </ li >
679+ ) ;
680+ } ) }
681+ </ ul >
682+ </ div >
683+ ) }
684+
685+ { scriptConflicts . length > 0 && (
686+ < div >
687+ < strong > Scripts ({ scriptConflicts . length } ):</ strong >
688+ < ul style = { { marginTop : '5px' , marginBottom : '0' , paddingLeft : '20px' } } >
689+ { scriptConflicts . map ( conflict => {
690+ const scriptName = conflict . local ?. name || conflict . server ?. name || '' ;
691+ return (
692+ < li key = { conflict . id } >
693+ { scriptName || 'Unnamed script' }
694+ < span style = { { fontFamily : 'monospace' , fontSize : '12px' , color : '#666' , marginLeft : '8px' } } >
695+ [{ conflict . id } ]
696+ </ span >
697+ </ li >
698+ ) ;
699+ } ) }
700+ </ ul >
701+ </ div >
702+ ) }
703+ </ div >
704+
705+ < p style = { { marginTop : '15px' , fontWeight : 'bold' } } >
706+ Do you want to overwrite the server settings with your local settings?
707+ </ p >
708+ </ div >
709+ ) ;
710+
711+ return (
712+ < Modal
713+ type = { Modal . Types . DANGER }
714+ icon = "warn-outline"
715+ title = "Migration Conflicts Detected"
716+ subtitle = "Settings with the same ID already exist on the server"
717+ confirmText = "Overwrite Server Settings"
718+ cancelText = "Cancel Migration"
719+ onConfirm = { ( ) => this . handleConflictOverwrite ( ) }
720+ onCancel = { ( ) => this . handleConflictCancel ( ) }
721+ >
722+ { conflictList }
723+ </ Modal >
724+ ) ;
725+ }
726+
574727 renderContent ( ) {
575728 return (
576729 < FlowView
0 commit comments