Repository: wagtail/wagtail

Issue:
Add ability for Settings (contrib) icons to be reflected on the Setting edit views
### Is your proposal related to a problem?

When registering a setting (either site specific or generic), there is the option to register it with an icon like follows:

```py
@register_setting(icon="site")
class SiteSettings(BaseSiteSetting):
    # ...
```
> See https://docs.wagtail.org/en/stable/reference/contrib/settings.html#appearance

However, this icon is only used in the menu and not the actual edit view of the same setting.

![Screenshot 2024-03-24 at 4 49 30 pm](https://github.com/wagtail/wagtail/assets/1396140/16141736-1733-4c7a-98cd-4ee45620f374)

![Screenshot 2024-03-24 at 4 49 34 pm](https://github.com/wagtail/wagtail/assets/1396140/9aa13710-72be-4dde-af7c-b02403cba264)

### Describe the solution you'd like

It would be great to ensure that the settings' edit view header had access to this same registered icon and then show it when editing the setting.

This would help editors have a visual confirmation that they have clicked the right thing, especially when there are multiple settings available.

A simple way to do this would be to add an `icon` property to the `Model` when `register_setting` is used, however this risks breaking any field also named `icon`. Maybe we need a registry of settings models & icons somehow added to `wagtail/contrib/settings/registry.py` and then used in `wagtail/contrib/settings/views.py` when setting up the views.

### Describe alternatives you've considered

Leave as is, this is not a critical item, just a nice UX improvement.

### Additional context

- There is a similar request to have the ability to set the icon on a Page model here https://github.com/wagtail/wagtail/issues/9652
- Snippets already have support for icons to be used across the menu and whenever being edited/viewed or chosen/managed.
- An older PR was prepared for this https://github.com/wagtail/wagtail/pull/8650 - but since it was created it got a bit behind main (without a review) and now is out of sync with a lot of recent improvements and simplifications of how header icons work.

### Working on this

Anyone can contribute to this, it would be recommended to suggest an approach on the issue before digging too deep.

Dig into how Snippets set their icons and also the proposed Page approaches here https://github.com/wagtail/wagtail/issues/9652#issuecomment-1324760712 for inspiration.

View our [contributing guidelines](https://docs.wagtail.org/en/latest/contributing/index.html), add a comment to the issue once you’re ready to start.



Discussion:

Resolving patch:
diff --git a/wagtail/contrib/settings/registry.py b/wagtail/contrib/settings/registry.py
--- a/wagtail/contrib/settings/registry.py
+++ b/wagtail/contrib/settings/registry.py
@@ -67,12 +67,17 @@ def construct_edit_url(self, instance):
 
 
 class Registry(list):
-    def register(self, model, **kwargs):
+    def __init__(self):
+        self._model_icons = {}
+
+    def register(self, model, icon="cog", **kwargs):
         from .models import BaseGenericSetting, BaseSiteSetting
 
         """
         Register a model as a setting, adding it to the wagtail admin menu
         """
+        if icon:
+            self._model_icons[model] = icon
 
         # Don't bother registering this if it is already registered
         if model in self:
@@ -82,7 +87,7 @@ def register(self, model, **kwargs):
         # Register a new menu item in the settings menu
         @hooks.register("register_settings_menu_item")
         def menu_hook():
-            return SettingMenuItem(model, **kwargs)
+            return SettingMenuItem(model, icon=self._model_icons.get(model), **kwargs)
 
         @hooks.register("register_permissions")
         def permissions_hook():
@@ -113,13 +118,13 @@ def permissions_hook():
 
         return model
 
-    def register_decorator(self, model=None, **kwargs):
+    def register_decorator(self, model=None, icon="cog", **kwargs):
         """
         Register a model as a setting in the Wagtail admin
         """
         if model is None:
-            return lambda model: self.register(model, **kwargs)
-        return self.register(model, **kwargs)
+            return lambda model: self.register(model, icon=icon, **kwargs)
+        return self.register(model, icon=icon, **kwargs)
 
     def get_by_natural_key(self, app_label, model_name):
         """
diff --git a/wagtail/contrib/settings/views.py b/wagtail/contrib/settings/views.py
--- a/wagtail/contrib/settings/views.py
+++ b/wagtail/contrib/settings/views.py
@@ -80,7 +80,6 @@ def redirect_to_relevant_instance(request, app_name, model_name):
 
 class EditView(generic.EditView):
     template_name = "wagtailsettings/edit.html"
-    header_icon = "cogs"
     error_message = gettext_lazy("The setting could not be saved due to errors.")
 
     def setup(self, request, app_name, model_name, *args, **kwargs):
@@ -129,11 +128,14 @@ def get_context_data(self, **kwargs):
 
         media = form.media + edit_handler.media
 
+        header_icon = registry._model_icons.get(self.model)
+
         context.update(
             {
                 "edit_handler": edit_handler,
                 "site_switcher": site_switcher,
                 "media": media,
+                "header_icon": header_icon,
             }
         )
 


Test patch:
diff --git a/wagtail/contrib/settings/tests/generic/test_admin.py b/wagtail/contrib/settings/tests/generic/test_admin.py
--- a/wagtail/contrib/settings/tests/generic/test_admin.py
+++ b/wagtail/contrib/settings/tests/generic/test_admin.py
@@ -126,6 +126,15 @@ def test_non_existent_model(self):
         )
         self.assertEqual(response.status_code, 404)
 
+    def test_register_with_icon(self):
+        edit_url = reverse("wagtailsettings:edit", args=("tests", "IconGenericSetting"))
+        edit_response = self.client.get(edit_url, follow=True)
+
+        self.assertContains(
+            edit_response,
+            """<svg class="icon icon-icon-setting-tag w-header__glyph" aria-hidden="true"><use href="#icon-icon-setting-tag"></use></svg>""",
+        )
+
     def test_edit_invalid(self):
         response = self.post(post_data={"foo": "bar"})
         self.assertContains(response, "The setting could not be saved due to errors.")
diff --git a/wagtail/contrib/settings/tests/site_specific/test_admin.py b/wagtail/contrib/settings/tests/site_specific/test_admin.py
--- a/wagtail/contrib/settings/tests/site_specific/test_admin.py
+++ b/wagtail/contrib/settings/tests/site_specific/test_admin.py
@@ -126,6 +126,15 @@ def test_non_existent_model(self):
         )
         self.assertEqual(response.status_code, 404)
 
+    def test_register_with_icon(self):
+        edit_url = reverse("wagtailsettings:edit", args=("tests", "IconGenericSetting"))
+        edit_response = self.client.get(edit_url, follow=True)
+
+        self.assertContains(
+            edit_response,
+            """<svg class="icon icon-icon-setting-tag w-header__glyph" aria-hidden="true"><use href="#icon-icon-setting-tag"></use></svg>""",
+        )
+
     def test_edit_invalid(self):
         response = self.post(post_data={"foo": "bar"})
         self.assertContains(response, "The setting could not be saved due to errors.")