How to extend Wagtail page listing header to have visible buttons
Tags: wagtail, django, python, webdev
Published: 2023-08-09 16:47:28

**1. We need to have custom `page_header_buttons` tag** to get all the action buttons in different form than as dropdown and not to mess up with the rest of templates:

In `/app/templatetags/custom_tags.py`:

```python
from django import template
from wagtail import hooks
from wagtail.admin.templatetags.wagtailadmin_tags import page_header_buttons

register = template.Library()

@register.inclusion_tag(
    "wagtailadmin/pages/listing/custom_page_header_buttons.html", takes_context=True
)
def custom_page_header_buttons(context, page, page_perms):
    return page_header_buttons(context, page, page_perms)
```

**2. Now let's make custom buttons template** in `app/wagtailadmin/pages/listing/custom_page_header_buttons.html` (we can use `core.css` `actionbutton` class to style buttons):

```twig
{% load wagtailadmin_tags i18n %}
<nav aria-label="{{ title }}">
        {% block content %}
            {% for button in buttons %}
            <div class="actionbutton mx-2 inline-block">
                <a href="{{ button.url }}" aria-label="{{ button.attrs.title }}" class="button bicolor button--icon">
                    {% if button.icon_name %}
                        <span class="icon-wrapper">
                            {% icon name=button.icon_name %}
                        </span>
                    {% endif %}
                    {{ button.label }}
                </a>
            </div>
            {% endfor %}
        {% endblock %}
</nav>
```

**3. Let's make breadcrumbs always extended** by creating `app/templates/wagtailadmin/shared/breadcrumbs.html` and removing `button`, `is_expanded` conditions and `hidden` classes:

```twig
{% load i18n wagtailadmin_tags %}
{% comment "text/markdown" %}
    The breadcrumb component is reused across all of Wagtail’s headers when the page tree context is needed.
    Variables this template accepts:

    `pages` - A list of wagtail page objects
    `trailing_breadcrumb_title` (string?) - use this for a non linkable last breadcrumb
    `classname` - Modifier classes
{% endcomment %}
{% with breadcrumb_link_classes='w-flex w-items-center w-h-full w-text-text-label w-pr-0.5 w-text-14 w-no-underline w-outline-offset-inside hover:w-underline hover:w-text-text-label w-h-full' breadcrumb_item_classes='w-h-full w-flex w-items-center w-overflow-hidden w-transition w-duration-300 w-whitespace-nowrap w-flex-shrink-0 w-font-bold' icon_classes='w-w-4 w-h-4 w-ml-3' %}
    {# Breadcrumbs are visible on mobile by default but hidden on desktop #}
    <div class="w-breadcrumb w-flex w-flex-row w-items-center w-overflow-x-auto w-overflow-y-hidden w-scrollbar-thin {{ classname }} w-pl-3" data-breadcrumb-next{% if not pages %} hidden{% endif %}>
        <div class="w-relative w-h-slim-header w-mr-4 w-top-0 w-z-20 w-flex w-items-center w-flex-row w-flex-1 sm:w-flex-none w-transition w-duration-300">
            <nav class="w-flex w-items-center w-flex-row w-h-full"
                aria-label="{% trans 'Breadcrumb' %}">
                <ol class="w-flex w-flex-row w-justify-start w-items-center w-h-full w-pl-0 w-my-0 w-gap-2 sm:w-gap-0 sm:w-space-x-2">
                    {% for page in pages %}
                        {% if page.is_root and url_root_name %}
                            {% url url_root_name as breadcrumb_url %}
                        {% else %}
                            {% url url_name page.id as breadcrumb_url %}
                        {% endif %}
                        {% if page.is_root %}
                            <li
                                class="{{ breadcrumb_item_classes }}"
                                data-breadcrumb-item
                            >
                                <a
                                    class="{{ breadcrumb_link_classes }}"
                                    href="{{ breadcrumb_url }}{{ querystring_value }}"
                                >
                                    {% trans "Root" %}
                                </a>
                                {% icon name="arrow-right" classname=icon_classes %}
                            </li>
                        {% elif forloop.first %}
                            {# For limited-permission users whose breadcrumb starts further down from the root #}
                            <li
                                class="{{ breadcrumb_item_classes }}"
                                data-breadcrumb-item
                            >
                                <a class="{{ breadcrumb_link_classes }}" href="{{ breadcrumb_url }}{{ querystring_value }}">
                                    {% trans "Root" %}
                                </a>
                                {% icon name="arrow-right" classname=icon_classes %}
                            </li>
                        {% elif forloop.last %}
                            <li
                                class="{{ breadcrumb_item_classes }}"
                                    data-breadcrumb-item
                           >
                                <a class="{{ breadcrumb_link_classes }}"
                                    href="{{ breadcrumb_url }}{{ querystring_value }}">
                                    {{ page.get_admin_display_title }}
                                </a>
                                {% if trailing_breadcrumb_title %}
                                    {% icon name="arrow-right" classname=icon_classes %}
                                {% endif %}
                            </li>
                        {% else %}
                            <li
                                class="{{ breadcrumb_item_classes }}"
                                data-breadcrumb-item
                            >
                                <a class="{{ breadcrumb_link_classes }}" href="{{ breadcrumb_url }}{{ querystring_value }}">
                                    {{ page.get_admin_display_title }}
                                </a>
                                {% icon name="arrow-right" classname=icon_classes %}
                            </li>
                        {% endif %}
                    {% endfor %}
                    {% if trailing_breadcrumb_title %}
                        <li class="{{ breadcrumb_item_classes }}">
                            <div class="w-flex w-justify-start w-items-center">
                                {{ trailing_breadcrumb_title }}
                            </div>
                        </li>
                    {% endif %}
                </ol>
            </nav>
        </div>
    </div>
{% endwith %}
```

**4. We need to make a new block in `slim_header.html`** because default ones cannot be used to have full width div with content. 

We need to copy & paste `wagtailadmin/templates/shared/headers/slim_header.html` file and add a new block at the end. Do not modify anything else, since it's a general template used for many purposes:

`app/templates/wagtailadmin/shared/headers/slim_header.html`:

```twig
{% load wagtailadmin_tags i18n %}
{% fragment as nav_icon_classes %}w-w-4 w-h-4 group-hover:w-transform group-hover:w-scale-110{% endfragment %}
{% fragment as nav_icon_button_classes %}w-w-slim-header w-h-slim-header w-bg-transparent w-border-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-text-meta w-transition w-group hover:w-text-text-label focus:w-text-text-label expanded:w-text-text-label expanded:w-border-y-2 expanded:w-border-b-current w-shrink-0{% endfragment %}
{% fragment as nav_icon_counter_classes %}-w-mr-3 w-py-0.5 w-px-[0.325rem] w-translate-y-[-8px] rtl:w-translate-x-[4px] w-translate-x-[-4px] w-text-[0.5625rem] w-font-bold w-text-text-button w-border w-border-surface-page w-rounded-[1rem]{% endfragment %}
{# Z index 99 to ensure header is always above  #}
<style>
    // for smaller header bar
    .w-slim-header {
        height: 50px;
    }
   
    // smaller buttons
    @media screen { .button {
        height: 2rem !important;
        line-height: 1.8rem;
    }}

</style>
<header class="w-slim-header w-flex w-flex-col sm:w-flex-row w-items-center w-justify-between w-bg-surface-header w-border-b w-border-border-furniture w-px-0 w-py-0 w-mb-0 w-relative w-top-0 w-z-header sm:w-sticky w-min-h-slim-header">

    {# Padding left on mobile to give space for navigation toggle, #}
    <div class="w-pl-slim-header sm:w-pl-5 w-min-h-slim-header sm:w-pr-2 w-w-full w-flex-1 w-overflow-x-auto w-box-border">
        <div class="w-flex w-flex-1 w-items-center w-overflow-hidden">
            {% block header_content %}
            {% endblock %}
        </div>
    </div>

    <div class="w-w-full sm:w-w-min w-flex sm:w-flex-nowrap sm:w-flex-row w-items-center w-p-0 sm:w-py-0 sm:w-pr-4 sm:w-justify-end">
        {% block actions %}
        {% endblock %}
    </div>
</header>
{% block after_header %}
{% endblock %}
```

**5. Finally we can create copy & paste `wagtailadmin/pages/page_listing_header.html`** to `app/templates/wagtailadmin/pages/page_listing_header.html` and make necessary modifications:

```twig
{% extends 'wagtailadmin/shared/headers/slim_header.html' %}
{% load wagtailadmin_tags i18n %}
{# CUSTOM TAG! #}
{% load custom_tags %}

{% block header_content %}
    {# Accessible page title #}
    <h1 class="w-sr-only">
        {{ title }}
    </h1>
    {# breadcrumbs #}
    {% breadcrumbs parent_page 'wagtailadmin_explore' url_root_name='wagtailadmin_explore_root' is_expanded=parent_page.is_root classname='sm:w-py-3 lg:w-py-7' %}
    {# Actions divider #}
    <div class="w-w-px w-h-[30px] w-ml-auto sm:w-ml-0 w-bg-border-furniture"></div>
    {# NOTHING MORE HERE! #}
{% endblock %}
{% block actions %}
    {% if not parent_page.is_root %}
        {% include "wagtailadmin/shared/side_panel_toggles.html" %}
        {# Page history #}
        {% if parent_page.get_latest_revision %}
            <a href="{% url 'wagtailadmin_pages:history' parent_page.id %}"
                class="{{ nav_icon_button_classes }}"
                data-tippy-content="{% trans 'History' %}"
                data-tippy-offset="[0, 0]"
                data-tippy-placement="bottom"
                aria-label="{% trans 'History' %}"
            >
                {% icon name="history" classname=nav_icon_classes %}
            </a>
        {% endif %}

        {% include "wagtailadmin/shared/page_status_tag_new.html" with page=parent_page %}
    {% endif %}
{% endblock %}
{% block after_header %}
{# OUR DIV WITH TITLE AND BUTTONS! #}
<div class="under-header w-mx-2 w-my-2">
    <h1 class="sr-hidden">{{ title }}</h1>
    {% custom_page_header_buttons parent_page page_perms=page_perms  %}
</div>
{% endblock %}
```

This still needs a lot of styling but works. I'll publish full source of ready-to-go when I'll finish it. ;)