@@ -41,24 +41,32 @@ type PausedMapSignal = RwSignal<HashMap<String, String>>;
4141/// whether it has a catalog host configured, and any autosync pause
4242/// message. Returns `None` for a healthy row (no third line, not red).
4343///
44- /// Pure so it can be unit-tested. Mirrors the detail-page status banner:
45- /// a `paused` row shows the refusal reason; an `error` row shows a
46- /// sign-in or no-remote hint depending on whether a remote host exists.
47- fn error_hint ( status : & str , has_host : bool , paused_message : Option < & str > ) -> Option < String > {
44+ /// The guidance line shown above a paused row's reason: paused rows stay
45+ /// paused until the user acts, so it names the resume action rather than
46+ /// leaving them to wonder why autosync stopped.
47+ const PAUSED_GUIDANCE : & str = "Autosync paused. Resolve the issue, then push manually to resume." ;
48+
49+ /// The one or two hint lines shown under a row's URI, error-coloured; empty
50+ /// when the row is healthy (which is also the not-red condition).
51+ ///
52+ /// Pure so it can be unit-tested. A `paused` row shows the guidance line
53+ /// followed by the raw refusal reason (when one is known); an `error` row
54+ /// shows a sign-in or no-remote hint depending on whether a remote host
55+ /// exists.
56+ fn hint_lines ( status : & str , has_host : bool , paused_message : Option < & str > ) -> Vec < String > {
4857 if status == "paused" || paused_message. is_some ( ) {
49- return Some ( match paused_message {
50- Some ( msg) => format ! ( "Autosync paused: {msg}" ) ,
51- None => "Autosync paused" . to_string ( ) ,
52- } ) ;
58+ let mut lines = vec ! [ PAUSED_GUIDANCE . to_string( ) ] ;
59+ lines. extend ( paused_message. map ( str:: to_string) ) ;
60+ return lines;
5361 }
5462 if status == "error" {
55- return Some ( if has_host {
63+ return vec ! [ if has_host {
5664 "Unable to check remote status — sign in again" . to_string( )
5765 } else {
5866 "No remote configured" . to_string( )
59- } ) ;
67+ } ] ;
6068 }
61- None
69+ Vec :: new ( )
6270}
6371
6472// ── Installed Packages List page ──
@@ -330,14 +338,14 @@ fn PackageItem(
330338 let namespace_display = data. namespace . clone ( ) ;
331339 let remote_display = data. remote_display . clone ( ) ;
332340
333- // Third-line attention hint. Red state and the reason line are both
334- // driven by `hint`: it is `Some` exactly when the row needs attention
335- // (autosync-paused, or a remote error), `None` for a healthy row.
341+ // Attention hint lines under the URI . Red state and the lines are both
342+ // driven by `hint`: it is non-empty exactly when the row needs attention
343+ // (autosync-paused, or a remote error), empty for a healthy row.
336344 let has_host = data. uri . as_ref ( ) . and_then ( util:: host_str) . is_some ( ) ;
337345 let ns_for_hint = data. namespace . clone ( ) ;
338346 let hint = Signal :: derive ( move || {
339347 let paused_message = paused_map. with ( |map| map. get ( & ns_for_hint) . cloned ( ) ) ;
340- status. with ( |s| error_hint ( s, has_host, paused_message. as_deref ( ) ) )
348+ status. with ( |s| hint_lines ( s, has_host, paused_message. as_deref ( ) ) )
341349 } ) ;
342350
343351 // Build menu buttons
@@ -353,10 +361,10 @@ fn PackageItem(
353361 ) ;
354362
355363 view ! {
356- <li class=move || if hint. get( ) . is_some( ) {
357- "qui-installed-package-item error"
358- } else {
364+ <li class=move || if hint. with( Vec :: is_empty) {
359365 "qui-installed-package-item"
366+ } else {
367+ "qui-installed-package-item error"
360368 } >
361369 <a class="link" href=pkg_href>
362370 <span class="item-primary" >{ namespace_display} </span>
@@ -366,9 +374,9 @@ fn PackageItem(
366374 { uri}
367375 </span>
368376 } ) }
369- { move || hint. get( ) . map( |h | view! {
370- <span class="item-error-hint" >{ h } </span>
371- } ) }
377+ { move || hint. get( ) . into_iter ( ) . map( |line | view! {
378+ <span class="item-error-hint" >{ line } </span>
379+ } ) . collect :: < Vec <_>> ( ) }
372380 </a>
373381 <Show when=move || refreshing. get( ) >
374382 <div class="q-spinner-inline" />
@@ -695,59 +703,65 @@ fn CreatePackagePopup(
695703
696704#[ cfg( test) ]
697705mod tests {
698- use super :: error_hint ;
706+ use super :: { PAUSED_GUIDANCE , hint_lines } ;
699707
700708 #[ test]
701- fn paused_with_reason_shows_autosync_paused_hint ( ) {
709+ fn paused_with_reason_shows_guidance_then_reason ( ) {
702710 assert_eq ! (
703- error_hint( "paused" , true , Some ( "workflow rejected metadata" ) ) ,
704- Some ( "Autosync paused: workflow rejected metadata" . to_string( ) )
711+ hint_lines( "paused" , true , Some ( "workflow rejected metadata" ) ) ,
712+ vec![
713+ PAUSED_GUIDANCE . to_string( ) ,
714+ "workflow rejected metadata" . to_string( ) ,
715+ ]
705716 ) ;
706717 // A snapshot-seeded pause carries its reason even when the row's
707718 // own status string was refreshed to something else on mount.
708719 assert_eq ! (
709- error_hint ( "up_to_date" , false , Some ( "hash mismatch" ) ) ,
710- Some ( "Autosync paused: hash mismatch". to_string( ) )
720+ hint_lines ( "up_to_date" , false , Some ( "hash mismatch" ) ) ,
721+ vec! [ PAUSED_GUIDANCE . to_string ( ) , " hash mismatch". to_string( ) ]
711722 ) ;
712723 }
713724
714725 #[ test]
715- fn paused_without_reason_falls_back_to_generic ( ) {
726+ fn paused_without_reason_shows_guidance_only ( ) {
716727 assert_eq ! (
717- error_hint ( "paused" , true , None ) ,
718- Some ( "Autosync paused" . to_string( ) )
728+ hint_lines ( "paused" , true , None ) ,
729+ vec! [ PAUSED_GUIDANCE . to_string( ) ]
719730 ) ;
720731 }
721732
722733 #[ test]
723- fn paused_reason_takes_precedence_over_error_status ( ) {
734+ fn paused_takes_precedence_over_error_status ( ) {
724735 // A row that is both `error` and has a pause reason shows the pause
725- // reason — the more specific, actionable message wins.
736+ // guidance + reason — the more specific, actionable message wins.
726737 assert_eq ! (
727- error_hint( "error" , true , Some ( "workflow rejected metadata" ) ) ,
728- Some ( "Autosync paused: workflow rejected metadata" . to_string( ) )
738+ hint_lines( "error" , true , Some ( "workflow rejected metadata" ) ) ,
739+ vec![
740+ PAUSED_GUIDANCE . to_string( ) ,
741+ "workflow rejected metadata" . to_string( ) ,
742+ ]
729743 ) ;
730744 }
731745
732746 #[ test]
733747 fn error_with_host_prompts_sign_in ( ) {
734748 assert_eq ! (
735- error_hint ( "error" , true , None ) ,
736- Some ( "Unable to check remote status — sign in again" . to_string( ) )
749+ hint_lines ( "error" , true , None ) ,
750+ vec! [ "Unable to check remote status — sign in again" . to_string( ) ]
737751 ) ;
738752 }
739753
740754 #[ test]
741755 fn error_without_host_reports_no_remote ( ) {
742756 assert_eq ! (
743- error_hint ( "error" , false , None ) ,
744- Some ( "No remote configured" . to_string( ) )
757+ hint_lines ( "error" , false , None ) ,
758+ vec! [ "No remote configured" . to_string( ) ]
745759 ) ;
746760 }
747761
748762 #[ test]
749763 fn healthy_row_has_no_hint ( ) {
750- assert_eq ! ( error_hint ( "up_to_date" , true , None ) , None ) ;
751- assert_eq ! ( error_hint ( "ahead" , false , None ) , None ) ;
764+ assert ! ( hint_lines ( "up_to_date" , true , None ) . is_empty ( ) ) ;
765+ assert ! ( hint_lines ( "ahead" , false , None ) . is_empty ( ) ) ;
752766 }
753767}
0 commit comments