Repository: zulip/zulip
Pull request #34594: Add support for reactivating groups.
Author: login: sahil839
Labels: name: size: XL
name: area: settings (user groups)
description: User groups related settings issues
Created: 2025-05-09T07:22:13Z | Merged: 2025-05-14T20:25:13Z

<!-- Describe your pull request here.-->
- First commit adds API support for reactivating groups.
- Second commit adds UI to reactivate group.
- Third commit is to live update UI when deactivating group.
- Last commit is a small alignment fix.

Fixes: <!-- Issue link, or clear description.--> #23568.

<!-- If the PR makes UI changes, always include one or more still screenshots to demonstrate your changes. If it seems helpful, add a screen capture of the new functionality as well.

Tooling tips: https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html
-->

**Screenshots and screen captures:**
![image](https://github.com/user-attachments/assets/84f1198c-ce0b-4808-a351-1fc8caeddd44)
![Peek 2025-05-09 13-01](https://github.com/user-attachments/assets/74cc16ad-24d6-4449-a0f2-6e0cc1bd9913)

<details>
<summary>Self-review checklist</summary>

<!-- Prior to submitting a PR, follow our step-by-step guide to review your own code:
https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#how-to-review-code -->

<!-- Once you create the PR, check off all the steps below that you have completed.
If any of these steps are not relevant or you have not completed, leave them unchecked.-->

- [ ] [Self-reviewed](https://zulip.readthedocs.io/en/latest/contributing/code-reviewing.html#how-to-review-code) the changes for clarity and maintainability
      (variable names, code reuse, readability, etc.).

Communicate decisions, questions, and potential concerns.

- [ ] Explains differences from previous plans (e.g., issue description).
- [ ] Highlights technical choices and bugs encountered.
- [ ] Calls out remaining decisions and concerns.
- [ ] Automated tests verify logic where appropriate.

Individual commits are ready for review (see [commit discipline](https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html)).

- [ ] Each commit is a coherent idea.
- [ ] Commit message(s) explain reasoning and motivation for changes.

Completed manual review and testing of the following:

- [ ] Visual appearance of the changes.
- [ ] Responsiveness and internationalization.
- [ ] Strings and tooltips.
- [ ] End-to-end functionality of buttons, interactions and flows.
- [ ] Corner cases, error conditions, and easily imagined bugs.
</details>



Diff:
diff --git a/api_docs/changelog.md b/api_docs/changelog.md
index 0bffb2dbd6d77..9f9a9a4705c46 100644
--- a/api_docs/changelog.md
+++ b/api_docs/changelog.md
@@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
 
 ## Changes in Zulip 11.0
 
+**Feature level 386**
+
+* [`PATCH /user_groups/{user_group_id}`](/api/update-user-group):
+  Added support to reactivate groups by passing `deactivated`
+  parameter as `False`.
+
 **Feature level 385**
 
 * [`POST /register`](/api/register-queue), [`PATCH/settings`](/api/update-settings),
diff --git a/version.py b/version.py
index 88e34c24036d6..04c8096890d24 100644
--- a/version.py
+++ b/version.py
@@ -34,7 +34,7 @@
 # new level means in api_docs/changelog.md, as well as "**Changes**"
 # entries in the endpoint's documentation in `zulip.yaml`.
 
-API_FEATURE_LEVEL = 385
+API_FEATURE_LEVEL = 386
 
 # Bump the minor PROVISION_VERSION to indicate that folks should provision
 # only when going from an old version of the code to a newer version. Bump
diff --git a/web/src/user_group_edit.ts b/web/src/user_group_edit.ts
index 88f6b0a36ade4..9570b9f97356f 100644
--- a/web/src/user_group_edit.ts
+++ b/web/src/user_group_edit.ts
@@ -21,6 +21,7 @@ import render_user_group_settings_overlay from "../templates/user_group_settings
 
 import * as blueslip from "./blueslip.ts";
 import * as browser_history from "./browser_history.ts";
+import * as buttons from "./buttons.ts";
 import * as channel from "./channel.ts";
 import * as components from "./components.ts";
 import type {Toggle} from "./components.ts";
@@ -210,20 +211,43 @@ function show_general_settings(group: UserGroup): void {
     update_general_panel_ui(group);
 }
 
-function update_general_panel_ui(group: UserGroup): void {
-    const $edit_container = get_edit_container(group);
+function update_deactivate_and_reactivate_buttons(group: UserGroup): void {
+    if (!settings_data.can_manage_user_group(group.id)) {
+        $(
+            `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .reactivate`,
+        ).hide();
+        $(
+            `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .deactivate`,
+        ).hide();
+        return;
+    }
 
-    if (settings_data.can_manage_user_group(group.id)) {
-        $edit_container.find(".group-header .button-group").show();
+    if (group.deactivated) {
         $(
             `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .deactivate`,
+        ).hide();
+        $(
+            `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .reactivate`,
         ).show();
     } else {
-        $edit_container.find(".group-header .button-group").hide();
         $(
             `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .deactivate`,
+        ).show();
+        $(
+            `.group_settings_header[data-group-id='${CSS.escape(group.id.toString())}'] .reactivate`,
         ).hide();
     }
+}
+
+function update_general_panel_ui(group: UserGroup): void {
+    const $edit_container = get_edit_container(group);
+
+    if (settings_data.can_manage_user_group(group.id)) {
+        $edit_container.find(".group-header .button-group").show();
+    } else {
+        $edit_container.find(".group-header .button-group").hide();
+    }
+    update_deactivate_and_reactivate_buttons(group);
     update_group_permission_settings_elements(group);
     update_group_membership_button(group.id);
 }
@@ -972,6 +996,25 @@ export function update_group_setting_in_permissions_panel(
     );
 }
 
+export function update_group_deactivated_banner(group: UserGroup): void {
+    if (!group.deactivated) {
+        $("#user_group_settings .group-banner").empty();
+        return;
+    }
+
+    const context = {
+        banner_type: compose_banner.WARNING,
+        classname: "group_deactivated",
+        hide_close_button: true,
+        banner_text: $t({
+            defaultMessage:
+                "This group is deactivated. It can't be mentioned or used for any permissions.",
+        }),
+    };
+
+    $("#user_group_settings .group-banner").html(render_modal_banner(context));
+}
+
 export function show_settings_for(group: UserGroup): void {
     const group_assigned_realm_permissions =
         settings_components.get_group_assigned_realm_permissions(group);
@@ -1021,19 +1064,7 @@ export function show_settings_for(group: UserGroup): void {
     show_membership_settings(group);
     show_general_settings(group);
 
-    const context = {
-        banner_type: compose_banner.WARNING,
-        classname: "group_deactivated",
-        hide_close_button: true,
-        banner_text: $t({
-            defaultMessage:
-                "This group is deactivated. It can't be mentioned or used for any permissions.",
-        }),
-    };
-
-    if (group.deactivated) {
-        $("#user_group_settings .group-banner").html(render_modal_banner(context));
-    }
+    update_group_deactivated_banner(group);
 
     $edit_container
         .find(".group-assigned-permissions")
@@ -1244,7 +1275,32 @@ export function handle_deleted_group(group_id: number): void {
     }
 
     if (is_editing_group(group_id)) {
+        const user_group = user_groups.get_user_group_from_id(group_id);
         $("#groups_overlay .deactivated-user-group-icon-right").show();
+
+        update_group_deactivated_banner(user_group);
+        update_deactivate_and_reactivate_buttons(user_group);
+        update_toggler_for_group_setting(user_group);
+        update_members_panel_ui(user_group);
+        update_group_membership_button(user_group.id);
+    }
+    redraw_user_group_list();
+}
+
+export function handle_reactivated_group(group_id: number): void {
+    if (!overlays.groups_open()) {
+        return;
+    }
+
+    if (is_editing_group(group_id)) {
+        const user_group = user_groups.get_user_group_from_id(group_id);
+        $("#groups_overlay .deactivated-user-group-icon-right").hide();
+
+        update_group_deactivated_banner(user_group);
+        update_deactivate_and_reactivate_buttons(user_group);
+        update_toggler_for_group_setting(user_group);
+        update_members_panel_ui(user_group);
+        update_group_membership_button(user_group.id);
     }
     redraw_user_group_list();
 }
@@ -1427,9 +1483,13 @@ export function update_group(event: UserGroupUpdateEvent, group: UserGroup): voi
         $group_row.find(".description").text(group.description);
     }
 
-    if (event.data.deactivated) {
-        $("#user-group-edit-filter-options").show();
-        handle_deleted_group(group.id);
+    if (event.data.deactivated !== undefined) {
+        update_filter_widget_visibility();
+        if (event.data.deactivated) {
+            handle_deleted_group(group.id);
+        } else {
+            handle_reactivated_group(group.id);
+        }
         return;
     }
 
@@ -1732,6 +1792,18 @@ function setup_dropdown_filters_widget(): void {
     filters_dropdown_widget.setup();
 }
 
+function update_filter_widget_visibility(): void {
+    if (user_groups.realm_has_deactivated_user_groups()) {
+        $("#user-group-edit-filter-options").show();
+    } else {
+        $("#user-group-edit-filter-options").hide();
+        update_displayed_groups(FILTERS.ACTIVE_GROUPS);
+        if (filters_dropdown_widget) {
+            filters_dropdown_widget.render(FILTERS.ACTIVE_GROUPS);
+        }
+    }
+}
+
 export function setup_page(callback: () => void): void {
     function initialize_components(): void {
         group_list_toggler = components.toggle({
@@ -1745,11 +1817,7 @@ export function setup_page(callback: () => void): void {
             },
         });
 
-        if (user_groups.realm_has_deactivated_user_groups()) {
-            $("#user-group-edit-filter-options").show();
-        } else {
-            $("#user-group-edit-filter-options").hide();
-        }
+        update_filter_widget_visibility();
         group_list_toggler.get().prependTo("#groups_overlay_container .list-toggler-container");
         setup_dropdown_filters_widget();
     }
@@ -1990,6 +2058,39 @@ export function initialize(): void {
         },
     );
 
+    $("#groups_overlay_container").on(
+        "click",
+        ".group_settings_header .reactivate-group-button",
+        function (this: HTMLElement) {
+            const active_group_data = get_active_data();
+            const group_id = active_group_data.id;
+            assert(group_id !== undefined);
+            const $button = $(this);
+            buttons.show_button_loading_indicator($button);
+            const data = {deactivated: JSON.stringify(false)};
+            channel.patch({
+                url: "/json/user_groups/" + group_id,
+                data,
+                success() {
+                    buttons.hide_button_loading_indicator($button);
+                },
+                error(xhr) {
+                    const message = channel.xhr_error_message($t({defaultMessage: "Failed"}), xhr);
+                    const context = {
+                        banner_type: compose_banner.ERROR,
+                        classname: "group-reactivation-error",
+                        hide_close_button: false,
+                        banner_text: message,
+                    };
+                    $("#user_group_settings .group-reactivation-error-banner").html(
+                        render_modal_banner(context),
+                    );
+                    buttons.hide_button_loading_indicator($button);
+                },
+            });
+        },
+    );
+
     function save_group_info(e: JQuery.ClickEvent): void {
         assert(e.currentTarget instanceof HTMLElement);
         const group = get_user_group_for_target(e.currentTarget);
@@ -2118,6 +2219,16 @@ export function initialize(): void {
             settings_org.discard_group_settings_subsection_changes($subsection, group);
         },
     );
+
+    $("#groups_overlay_container").on(
+        "click",
+        ".group-reactivation-error-banner .main-view-banner-close-button",
+        () => {
+            $(
+                "#user_group_settings .group-reactivation-error-banner .group-reactivation-error",
+            ).remove();
+        },
+    );
 }
 
 export function launch(
diff --git a/web/styles/subscriptions.css b/web/styles/subscriptions.css
index 53f389cff9294..65dc1f5ac2094 100644
--- a/web/styles/subscriptions.css
+++ b/web/styles/subscriptions.css
@@ -1005,7 +1005,7 @@ h4.user_group_setting_subsection_title {
     .stream_settings_header {
         white-space: nowrap;
         display: flex;
-        margin-left: 15px;
+        margin-left: 18px;
 
         .tab-container {
             .tab-switcher {
diff --git a/web/templates/user_group_settings/user_group_settings.hbs b/web/templates/user_group_settings/user_group_settings.hbs
index c0e7d7b76a6a5..3b03ef23ba158 100644
--- a/web/templates/user_group_settings/user_group_settings.hbs
+++ b/web/templates/user_group_settings/user_group_settings.hbs
@@ -10,16 +10,17 @@
               attention="quiet" intent="brand" }}
             {{/if}}
         </div>
-        {{#unless group.deactivated}}
         {{> ../components/icon_button icon="user-group-x" intent="danger" custom_classes="deactivate-group-button deactivate tippy-zulip-delayed-tooltip"
           data-tippy-content=(t 'Deactivate group') }}
-        {{/unless}}
+        {{> ../components/icon_button icon="user-group-plus" intent="success" custom_classes="reactivate-group-button reactivate tippy-zulip-delayed-tooltip"
+          data-tippy-content=(t 'Reactivate group') }}
     </div>
 </div>
 <div class="user_group_settings_wrapper" data-group-id="{{group.id}}">
     <div class="inner-box">
 
         <div class="group_general_settings group_setting_section" data-group-section="general">
+            <div class="group-reactivation-error-banner"></div>
             <div class="group-banner"></div>
             <div class="group-header">
                 <div class="group-name-wrapper">
diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml
index 03d3573390d60..24065da2bf896 100644
--- a/zerver/openapi/zulip.yaml
+++ b/zerver/openapi/zulip.yaml
@@ -22135,13 +22135,18 @@ paths:
         Update the name, description or any of the permission settings
         of a [user group](/help/user-groups).
 
+        This endpoint is also used to reactivate a user group.
+
         Note that while permissions settings of deactivated groups can
         be edited by this API endpoint, and those permissions settings
         do affect the ability to modify the deactivated group and its
         membership, the deactivated group itself cannot be mentioned
         or used in the value of any permission without first being reactivated.
 
-        **Changes**: Prior to Zulip 10.0 (feature level 340), only the name field
+        **Changes**: Starting with Zulip 11.0 (feature level ZF-61b9bf), this
+        endpoint can be used to reactivate a user group.
+
+        Prior to Zulip 10.0 (feature level 340), only the name field
         of deactivated groups could be modified.
       parameters:
         - $ref: "#/components/parameters/UserGroupId"
@@ -22293,6 +22298,18 @@ paths:
                           "old": 11,
                         }
                     - $ref: "#/components/schemas/GroupSettingValueUpdate"
+                deactivated:
+                  description: |
+                    A deactivated user group can be reactivated by passing this
+                    parameter as `false`.
+
+                    Passing `true` does nothing as user group is deactivated
+                    using [`POST /user_groups/{user_group_id}/deactivate`](deactivate-user-group)
+                    endpoint.
+
+                    **Changes**: New in Zulip 11.0 (feature level 386).
+                  type: boolean
+                  example: false
             encoding:
               can_add_members_group:
                 contentType: application/json
@@ -22306,6 +22323,8 @@ paths:
                 contentType: application/json
               can_remove_members_group:
                 contentType: application/json
+              deactivated:
+                contentType: application/json
       responses:
         "200":
           $ref: "#/components/responses/SimpleSuccess"
diff --git a/zerver/tests/test_user_groups.py b/zerver/tests/test_user_groups.py
index d6df82ed4c1ae..85334c0637336 100644
--- a/zerver/tests/test_user_groups.py
+++ b/zerver/tests/test_user_groups.py
@@ -1824,6 +1824,118 @@ def test_user_group_deactivation_with_group_used_for_settings(self) -> None:
                 leadership_group, setting_name, moderators_group, acting_user=None
             )
 
+    def test_user_group_reactivation(self) -> None:
+        support_group = self.create_user_group_for_test(
+            "support", acting_user=self.example_user("desdemona")
+        )
+        realm = get_realm("zulip")
+
+        admins_group = NamedUserGroup.objects.get(name=SystemGroups.ADMINISTRATORS, realm=realm)
+        do_change_realm_permission_group_setting(
+            realm,
+            "can_manage_all_groups",
+            admins_group,
+            acting_user=None,
+        )
+
+        do_deactivate_user_group(support_group, acting_user=None)
+        self.login("othello")
+        params = {"deactivated": orjson.dumps(False).decode()}
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_error(result, "Insufficient permission")
+
+        members_group = NamedUserGroup.objects.get(name=SystemGroups.MEMBERS, realm=realm)
+        do_change_realm_permission_group_setting(
+            realm,
+            "can_manage_all_groups",
+            members_group,
+            acting_user=None,
+        )
+
+        self.login("othello")
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_success(result)
+        support_group = NamedUserGroup.objects.get(name="support", realm=realm)
+        self.assertFalse(support_group.deactivated)
+
+        do_deactivate_user_group(support_group, acting_user=None)
+
+        # Check admins can deactivate groups even if they are not members
+        # of the group.
+        self.login("iago")
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_success(result)
+        support_group = NamedUserGroup.objects.get(name="support", realm=realm)
+        self.assertFalse(support_group.deactivated)
+
+        do_deactivate_user_group(support_group, acting_user=None)
+
+        # Check moderators can deactivate groups if they are allowed by
+        # can_manage_all_groups even when they are not members of the group.
+        admins_group = NamedUserGroup.objects.get(
+            name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True
+        )
+        do_change_realm_permission_group_setting(
+            realm,
+            "can_manage_all_groups",
+            admins_group,
+            acting_user=None,
+        )
+        self.login("shiva")
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_error(result, "Insufficient permission")
+
+        moderators_group = NamedUserGroup.objects.get(
+            name=SystemGroups.MODERATORS, realm=realm, is_system_group=True
+        )
+        do_change_realm_permission_group_setting(
+            realm,
+            "can_manage_all_groups",
+            moderators_group,
+            acting_user=None,
+        )
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_success(result)
+        support_group = NamedUserGroup.objects.get(name="support", realm=realm)
+        self.assertFalse(support_group.deactivated)
+
+        do_deactivate_user_group(support_group, acting_user=None)
+
+        do_change_realm_permission_group_setting(
+            realm,
+            "can_manage_all_groups",
+            admins_group,
+            acting_user=None,
+        )
+        do_change_user_group_permission_setting(
+            support_group, "can_manage_group", admins_group, acting_user=None
+        )
+
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_error(result, "Insufficient permission")
+
+        do_change_user_group_permission_setting(
+            support_group, "can_manage_group", moderators_group, acting_user=None
+        )
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_success(result)
+        support_group = NamedUserGroup.objects.get(name="support", realm=realm)
+        self.assertFalse(support_group.deactivated)
+
+        do_deactivate_user_group(support_group, acting_user=None)
+
+        setting_group = self.create_or_update_anonymous_group_for_setting(
+            [self.example_user("shiva")], [admins_group]
+        )
+        do_change_user_group_permission_setting(
+            support_group, "can_manage_group", setting_group, acting_user=None
+        )
+
+        result = self.client_patch(f"/json/user_groups/{support_group.id}", info=params)
+        self.assert_json_success(result)
+        support_group = NamedUserGroup.objects.get(name="support", realm=realm)
+        self.assertFalse(support_group.deactivated)
+
     def test_query_counts(self) -> None:
         hamlet = self.example_user("hamlet")
         cordelia = self.example_user("cordelia")
diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py
index c256002323727..1b22171c00213 100644
--- a/zerver/views/user_groups.py
+++ b/zerver/views/user_groups.py
@@ -13,6 +13,7 @@
     check_add_user_group,
     do_change_user_group_permission_setting,
     do_deactivate_user_group,
+    do_reactivate_user_group,
     do_update_user_group_description,
     do_update_user_group_name,
     remove_subgroups_from_user_group,
@@ -139,6 +140,7 @@ def edit_user_group(
     can_manage_group: Json[GroupSettingChangeRequest] | None = None,
     can_mention_group: Json[GroupSettingChangeRequest] | None = None,
     can_remove_members_group: Json[GroupSettingChangeRequest] | None = None,
+    deactivated: Json[bool] | None = None,
 ) -> HttpResponse:
     if (
         name is None
@@ -149,6 +151,7 @@ def edit_user_group(
         and can_manage_group is None
         and can_mention_group is None
         and can_remove_members_group is None
+        and deactivated is None
     ):
         raise JsonableError(_("No new data supplied"))
 
@@ -163,6 +166,9 @@ def edit_user_group(
     if description is not None and description != user_group.description:
         do_update_user_group_description(user_group, description, acting_user=user_profile)
 
+    if deactivated is not None and not deactivated and user_group.deactivated:
+        do_reactivate_user_group(user_group, acting_user=user_profile)
+
     request_settings_dict = locals()
     nobody_group = get_system_user_group_by_name(SystemGroups.NOBODY, user_profile.realm_id)
     for setting_name, permission_config in NamedUserGroup.GROUP_PERMISSION_SETTINGS.items():


Reviews:
user: login: sahil839
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T09:13:53Z
user: login: timabbott
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T20:18:47Z
user: login: timabbott
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T20:25:38Z

Review comments:
diff_hunk: @@ -163,6 +166,9 @@ def edit_user_group(
     if description is not None and description != user_group.description:
         do_update_user_group_description(user_group, description, acting_user=user_profile)
 
+    if deactivated is not None and not deactivated and user_group.deactivated:
path: zerver/views/user_groups.py
user: login: sahil839
body: Do we want to support deactivating groups by passing `deactivated` as `True` here along with the existing separate endpoint used for deactivation?
created_at: 2025-05-09T09:13:53Z
author_association: COLLABORATOR
diff_hunk: @@ -163,6 +166,9 @@ def edit_user_group(
     if description is not None and description != user_group.description:
         do_update_user_group_description(user_group, description, acting_user=user_profile)
 
+    if deactivated is not None and not deactivated and user_group.deactivated:
path: zerver/views/user_groups.py
user: login: timabbott
body: I'm not sure; this is an API design discussion. 
created_at: 2025-05-14T20:18:47Z
author_association: MEMBER
diff_hunk: @@ -163,6 +166,9 @@ def edit_user_group(
     if description is not None and description != user_group.description:
         do_update_user_group_description(user_group, description, acting_user=user_profile)
 
+    if deactivated is not None and not deactivated and user_group.deactivated:
path: zerver/views/user_groups.py
user: login: timabbott
body: Seems OK to merge this as-is and consider tweaking that as a follow-up. The main risk I see is managing issues around changing multiple settings in the same API request as reactivate/deactivation.
created_at: 2025-05-14T20:25:38Z
author_association: MEMBER

PR comments:
user: login: sahil839
created_at: 2025-05-09T07:36:37Z
body: GIF for how error is shown. I have just added a random error for testing this -
![Peek 2025-05-09 13-05](https://github.com/user-attachments/assets/ee94f87e-7bd9-49ed-97a4-089e34d1c770)

author_association: COLLABORATOR
user: login: sahil839
created_at: 2025-05-09T09:17:09Z
body: For error do we want a "x" button for hiding the banner?
author_association: COLLABORATOR
user: login: alya
created_at: 2025-05-10T06:36:10Z
body: > For error do we want a "x" button for hiding the banner?

Oh, maybe? Do we have a regular pattern for this, or is it a mix?
author_association: CONTRIBUTOR
user: login: sahil839
created_at: 2025-05-12T14:03:43Z
body: I checked and we show close buttons for compose box errors and for errors in alert words UI. Also, I think it is fine to have error button for error banners as it is not show information which we want to make sure that is visible to users all the time, so I added the close button.
author_association: COLLABORATOR
user: login: alya
created_at: 2025-05-15T15:59:13Z
body: @sahil839 Could you add the instructions for reactivating a group to the bottom of `/help/deactivate-a-user-group`?
author_association: CONTRIBUTOR
user: login: sahil839
created_at: 2025-05-16T03:12:56Z
body: Opened #34647.
author_association: COLLABORATOR