@@ -46,6 +46,10 @@ const ProjectSettings = (props) => {
4646 // newly-stored values and these derives recompute, letting
4747 // `showRetentionConfirmation` settle back to a clean state.
4848 const browserTz = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone || 'UTC' ;
49+ const persistedName = van . derive ( ( ) => props . name . val ?? '' ) ;
50+ const persistedUseWeights = van . derive ( ( ) => props . use_dq_score_weights . val ?? true ) ;
51+ const persistedObsUrl = van . derive ( ( ) => props . observability_api_url . val ?? '' ) ;
52+ const persistedObsKey = van . derive ( ( ) => props . observability_api_key . val ?? '' ) ;
4953 const persistedRetentionEnabled = van . derive ( ( ) => props . data_retention_enabled . val ?? false ) ;
5054 const persistedRetentionDays = van . derive ( ( ) => props . data_retention_days . val ?? 180 ) ;
5155 const persistedRetentionCron = van . derive ( ( ) => props . retention_cron_expr . val ?? '0 1 * * *' ) ;
@@ -66,10 +70,30 @@ const ProjectSettings = (props) => {
6670 observability_api_url : van . state ( true ) ,
6771 data_retention_days : van . state ( Number . isFinite ( form . data_retention_days . rawVal ) ) ,
6872 } ;
73+ // Retention is unchanged when the enabled flag matches the persisted value and,
74+ // while enabled, the days/cron/tz also match. When retention is off, days/cron/tz
75+ // are hidden and the backend clears them, so they don't count as unsaved changes —
76+ // only the enabled flag matters.
77+ const retentionUnchanged = van . derive ( ( ) => {
78+ if ( form . data_retention_enabled . val !== persistedRetentionEnabled . val ) return false ;
79+ if ( ! form . data_retention_enabled . val ) return true ;
80+ return form . data_retention_days . val === persistedRetentionDays . val
81+ && form . retention_cron_expr . val === persistedRetentionCron . val
82+ && form . retention_cron_tz . val === persistedRetentionTz . val ;
83+ } ) ;
84+ // No unsaved changes when every field matches its persisted value. Because the
85+ // persisted derives are reactive, this settles back to `true` after a Save once
86+ // the props update with the stored values, disabling the button again.
87+ const noChanges = van . derive ( ( ) => form . name . val === persistedName . val
88+ && form . use_dq_score_weights . val === persistedUseWeights . val
89+ && form . observability_api_url . val === persistedObsUrl . val
90+ && form . observability_api_key . val === persistedObsKey . val
91+ && retentionUnchanged . val ) ;
6992 const saveDisabled = van . derive ( ( ) => ! formValidity . name . val
7093 || ! formValidity . observability_api_url . val
7194 || ! formValidity . observability_api_key . val
72- || ( form . data_retention_enabled . val && ! formValidity . data_retention_days . val ) ) ;
95+ || ( form . data_retention_enabled . val && ! formValidity . data_retention_days . val )
96+ || noChanges . val ) ;
7397 const testObservabilityDisabled = van . derive ( ( ) => form . observability_api_url . val . length <= 0 || form . observability_api_key . val . length <= 0 ) ;
7498 const retentionCronEditorValue = van . derive ( ( ) => {
7599 if ( form . retention_cron_expr . val && form . retention_cron_tz . val && form . data_retention_enabled . val ) {
@@ -284,7 +308,7 @@ const ProjectSettings = (props) => {
284308 } ) ,
285309 ) ,
286310 div (
287- { class : 'flex-row fx-justify-content-flex-end' } ,
311+ { class : 'flex-row fx-justify-content-flex-end' , style : 'max-width: 700px;' } ,
288312 Button ( {
289313 type : 'stroked' ,
290314 color : 'primary' ,
0 commit comments