55import jmespath as jp
66from slack_bolt import Ack , App , BoltContext
77from slack_bolt .adapter .aws_lambda import SlackRequestHandler
8+ import slack_sdk .errors
89from slack_sdk import WebClient
910from slack_sdk .web .slack_response import SlackResponse
1011
@@ -808,7 +809,7 @@ def _get_cached_user_info(view_key: str, user_id: str, client: "WebClient") -> t
808809
809810
810811@app .action (slack_helpers .RequestForAccessView .ACCOUNT_ACTION_ID )
811- def handle_account_selection (ack : Ack , body : dict , client : WebClient ) -> SlackResponse :
812+ def handle_account_selection (ack : Ack , body : dict , client : WebClient ) -> SlackResponse | None :
812813 ack ()
813814 logger .info ("Handling account selection" )
814815
@@ -824,11 +825,35 @@ def handle_account_selection(ack: Ack, body: dict, client: WebClient) -> SlackRe
824825 logger .info (f"Selected accounts: { account_ids } " )
825826
826827 view_id = body ["view" ]["id" ]
828+ view_hash = body ["view" ].get ("hash" )
829+
830+ def safe_views_update (view ) -> SlackResponse | None : # noqa: ANN001, ANN202
831+ nonlocal view_hash
832+ try :
833+ response = client .views_update (view_id = view_id , view = view , hash = view_hash )
834+ except slack_sdk .errors .SlackApiError as e :
835+ error = e .response .get ("error" ) if e .response else None
836+ # hash_conflict / view_expired = a newer handler already updated the view; skip silently.
837+ if error in {"hash_conflict" , "view_expired" , "not_found" }:
838+ logger .info (f"Skipping stale views_update: { error } " )
839+ return None
840+ raise
841+ new_hash = response .data .get ("view" , {}).get ("hash" ) if response .data else None # type: ignore[union-attr]
842+ if new_hash :
843+ view_hash = new_hash
844+ return response
827845
828846 # No accounts selected → show placeholder, disable submit
829847 if not account_ids :
830848 updated_view = slack_helpers .RequestForAccessView .build_no_permission_sets_view (view_blocks = body ["view" ]["blocks" ])
831- return client .views_update (view_id = view_id , view = updated_view )
849+ return safe_views_update (updated_view )
850+
851+ # Immediately replace stale permission set list with a loading placeholder before any AWS calls.
852+ loading_response = safe_views_update (slack_helpers .RequestForAccessView .show_permission_set_loading (body ["view" ]["blocks" ]))
853+ if loading_response is None :
854+ # A newer handler is already in flight; let it produce the final view.
855+ return None
856+ current_blocks = loading_response .data ["view" ]["blocks" ] # type: ignore[index]
832857
833858 # Get cached user info, re-fetching from Identity Center on cache miss
834859 # (e.g. when Lambda container was recycled between form load and account selection)
@@ -842,8 +867,8 @@ def handle_account_selection(ack: Ack, body: dict, client: WebClient) -> SlackRe
842867 logger .info (f"Valid permission sets for selected accounts and user: { valid_ps_names } " )
843868
844869 if not valid_ps_names :
845- updated_view = slack_helpers .RequestForAccessView .build_no_permission_sets_view (view_blocks = body [ "view" ][ "blocks" ] )
846- return client . views_update ( view_id = view_id , view = updated_view )
870+ updated_view = slack_helpers .RequestForAccessView .build_no_permission_sets_view (view_blocks = current_blocks )
871+ return safe_views_update ( updated_view )
847872
848873 if "*" in valid_ps_names :
849874 permission_sets = sso .get_permission_sets_from_config_with_cache (sso_client = sso_client , s3_client = s3_client , cfg = cfg )
@@ -853,8 +878,8 @@ def handle_account_selection(ack: Ack, body: dict, client: WebClient) -> SlackRe
853878
854879 # Handle case where filtered list is empty (configured names don't exist in SSO)
855880 if not permission_sets :
856- updated_view = slack_helpers .RequestForAccessView .build_no_permission_sets_view (view_blocks = body [ "view" ][ "blocks" ] )
857- return client . views_update ( view_id = view_id , view = updated_view )
881+ updated_view = slack_helpers .RequestForAccessView .build_no_permission_sets_view (view_blocks = current_blocks )
882+ return safe_views_update ( updated_view )
858883
859884 # Classify permission sets as auto-approved vs requires-approval
860885 auto_approved_arns : set [str ] | None = None
@@ -882,12 +907,12 @@ def approver_group_resolver(group_ids: frozenset[str]) -> set[str]:
882907 )
883908
884909 updated_view = slack_helpers .RequestForAccessView .update_with_permission_sets (
885- view_blocks = body [ "view" ][ "blocks" ] ,
910+ view_blocks = current_blocks ,
886911 permission_sets = permission_sets ,
887912 display_names = cfg .permission_set_display_names ,
888913 auto_approved_arns = auto_approved_arns ,
889914 )
890- return client . views_update ( view_id = view_id , view = updated_view )
915+ return safe_views_update ( updated_view )
891916
892917
893918# Early Revoke Handlers
0 commit comments