Browse Source

Add breadcrumbs and new Page Editor side panels to Snippets views (#8623)

sag᠎e 2 years ago
parent
commit
952edd84c7
24 changed files with 399 additions and 105 deletions
  1. 1 1
      wagtail/admin/templates/wagtailadmin/pages/create.html
  2. 1 1
      wagtail/admin/templates/wagtailadmin/pages/edit.html
  3. 1 1
      wagtail/admin/templates/wagtailadmin/shared/headers/page_create_header.html
  4. 1 1
      wagtail/admin/templates/wagtailadmin/shared/headers/page_edit_header.html
  5. 0 0
      wagtail/admin/templates/wagtailadmin/shared/side_panel_toggles.html
  6. 0 0
      wagtail/admin/templates/wagtailadmin/shared/side_panels.html
  7. 2 2
      wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/locale.html
  8. 7 4
      wagtail/admin/ui/side_panels.py
  9. 48 3
      wagtail/admin/views/generic/models.py
  10. 43 0
      wagtail/snippets/side_panels.py
  11. 7 1
      wagtail/snippets/templates/wagtailsnippets/snippets/create.html
  12. 9 4
      wagtail/snippets/templates/wagtailsnippets/snippets/edit.html
  13. 43 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/_base_header.html
  14. 28 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/create_header.html
  15. 36 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/edit_header.html
  16. 17 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/history_header.html
  17. 16 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/list_header.html
  18. 17 0
      wagtail/snippets/templates/wagtailsnippets/snippets/headers/usage_header.html
  19. 7 0
      wagtail/snippets/templates/wagtailsnippets/snippets/history.html
  20. 61 0
      wagtail/snippets/templates/wagtailsnippets/snippets/side_panels/includes/status/workflow.html
  21. 4 4
      wagtail/snippets/templates/wagtailsnippets/snippets/type_index.html
  22. 7 3
      wagtail/snippets/templates/wagtailsnippets/snippets/usage.html
  23. 15 66
      wagtail/snippets/tests.py
  24. 28 14
      wagtail/snippets/views/snippets.py

+ 1 - 1
wagtail/admin/templates/wagtailadmin/pages/create.html

@@ -10,7 +10,7 @@
 
     <div class="w-sticky w-top-0 w-z-header">
         {% include 'wagtailadmin/shared/headers/page_create_header.html' %}
-        {% include "wagtailadmin/shared/page_side_panels.html" %}
+        {% include "wagtailadmin/shared/side_panels.html" %}
     </div>
 
     <form id="page-edit-form" action="{% url 'wagtailadmin_pages:add' content_type.app_label content_type.model parent_page.id %}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>

+ 1 - 1
wagtail/admin/templates/wagtailadmin/pages/edit.html

@@ -11,7 +11,7 @@
 
     <div class="w-sticky w-top-0 w-z-header">
         {% include 'wagtailadmin/shared/headers/page_edit_header.html' %}
-        {% include "wagtailadmin/shared/page_side_panels.html" %}
+        {% include "wagtailadmin/shared/side_panels.html" %}
     </div>
 
     {% block form %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/shared/headers/page_create_header.html

@@ -12,6 +12,6 @@
 
 {% block actions %}
     {% with nav_icon_classes='w-w-4 w-h-4' nav_icon_button_classes='w-h-[50px] w-bg-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-grey-400 w-transition hover:w-transform hover:w-scale-110 hover:w-text-primary focus:w-text-primary' %}
-        {% include "wagtailadmin/shared/page_side_panel_toggles.html" %}
+        {% include "wagtailadmin/shared/side_panel_toggles.html" %}
     {% endwith %}
 {% endblock %}

+ 1 - 1
wagtail/admin/templates/wagtailadmin/shared/headers/page_edit_header.html

@@ -16,7 +16,7 @@
 
 {% block actions %}
     {% with nav_icon_classes='w-w-4 w-h-4' nav_icon_button_classes='w-h-[50px] w-bg-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-grey-400 w-transition hover:w-transform hover:w-scale-110 hover:w-text-primary focus:w-text-primary' %}
-        {% include "wagtailadmin/shared/page_side_panel_toggles.html" %}
+        {% include "wagtailadmin/shared/side_panel_toggles.html" %}
 
         {# Page history #}
         {% if page.get_latest_revision %}

+ 0 - 0
wagtail/admin/templates/wagtailadmin/shared/page_side_panel_toggles.html → wagtail/admin/templates/wagtailadmin/shared/side_panel_toggles.html


+ 0 - 0
wagtail/admin/templates/wagtailadmin/shared/page_side_panels.html → wagtail/admin/templates/wagtailadmin/shared/side_panels.html


+ 2 - 2
wagtail/admin/templates/wagtailadmin/shared/side_panels/includes/status/locale.html

@@ -8,7 +8,7 @@
         {% trans 'Locale: ' as screen_reader_title_prefix %}
     {% endif %}
 
-    {% if translations %}
+    {% if object.pk and translations %}
         {% blocktrans trimmed asvar help_text %}
             Available in {{ translations_total }} locales
         {% endblocktrans %}
@@ -16,7 +16,7 @@
         {% trans 'No other translations' as help_text %}
     {% endif %}
 
-    {% with icon_name='globe' title=object.locale.get_display_name %}
+    {% with icon_name='globe' title=locale.get_display_name %}
         {{ block.super }}
     {% endwith %}
 {% endblock %}

+ 7 - 4
wagtail/admin/ui/side_panels.py

@@ -31,9 +31,12 @@ class BaseStatusSidePanel(BaseSidePanel):
     toggle_icon_name = "info-circle"
 
     def get_status_templates(self, context):
-        templates = [
-            "wagtailadmin/shared/side_panels/includes/status/workflow.html",
-        ]
+        templates = []
+
+        if self.object.pk:
+            templates += [
+                "wagtailadmin/shared/side_panels/includes/status/workflow.html",
+            ]
 
         if context.get("locale"):
             templates += ["wagtailadmin/shared/side_panels/includes/status/locale.html"]
@@ -50,7 +53,7 @@ class BaseStatusSidePanel(BaseSidePanel):
 class PageStatusSidePanel(BaseStatusSidePanel):
     def get_status_templates(self, context):
         templates = super().get_status_templates(context)
-        if self.object.id:
+        if self.object.pk:
             templates += ["wagtailadmin/shared/side_panels/includes/status/locked.html"]
         templates += ["wagtailadmin/shared/side_panels/includes/status/privacy.html"]
         return templates

+ 48 - 3
wagtail/admin/views/generic/models.py

@@ -18,9 +18,11 @@ from django.views.generic.list import BaseListView
 from wagtail.admin import messages
 from wagtail.admin.forms.search import SearchForm
 from wagtail.admin.panels import get_edit_handler
+from wagtail.admin.templatetags.wagtailadmin_tags import user_display_name
 from wagtail.admin.ui.tables import Table, TitleColumn
 from wagtail.log_actions import log
-from wagtail.models import RevisionMixin
+from wagtail.log_actions import registry as log_registry
+from wagtail.models import DraftStateMixin, RevisionMixin
 from wagtail.search.index import class_is_indexed
 
 from .base import WagtailAdminTemplateMixin
@@ -105,7 +107,7 @@ class IndexView(
     def get_search_url(self):
         if not self.is_searchable:
             return None
-        return self.get_index_url()
+        return self.index_url_name
 
     def get_search_form(self):
         if self.model is None:
@@ -314,6 +316,11 @@ class EditView(
     error_message = None
     submit_button_label = gettext_lazy("Save")
 
+    def setup(self, request, *args, **kwargs):
+        super().setup(request, *args, **kwargs)
+        self.revision_enabled = self.model and issubclass(self.model, RevisionMixin)
+        self.draftstate_enabled = self.model and issubclass(self.model, DraftStateMixin)
+
     def get_object(self, queryset=None):
         if "pk" not in self.kwargs:
             self.kwargs["pk"] = self.args[0]
@@ -353,7 +360,7 @@ class EditView(
         self.has_content_changes = self.form.has_changed()
 
         # Save revision if the model inherits from RevisionMixin
-        if isinstance(instance, RevisionMixin):
+        if self.revision_enabled:
             revision = instance.save_revision(
                 user=self.request.user,
                 changed=self.has_content_changes,
@@ -385,6 +392,38 @@ class EditView(
             return None
         return self.error_message
 
+    def get_live_last_updated_info(self):
+        # DraftStateMixin is applied but object is not live
+        if self.draftstate_enabled and not self.object.live:
+            return None
+
+        revision = None
+        # DraftStateMixin is applied and object is live
+        if self.draftstate_enabled and self.object.live_revision:
+            revision = self.object.live_revision
+        # RevisionMixin is applied, so object is assumed to be live
+        elif self.revision_enabled and self.object.latest_revision:
+            revision = self.object.latest_revision
+
+        # No mixin is applied or no revision exists, fall back to latest log entry
+        if not revision:
+            return log_registry.get_logs_for_instance(self.object).first()
+
+        return {
+            "timestamp": revision.created_at,
+            "user_display_name": user_display_name(revision.user),
+        }
+
+    def get_draft_last_updated_info(self):
+        if not (self.draftstate_enabled and self.object.has_unpublished_changes):
+            return None
+
+        revision = self.object.latest_revision
+        return {
+            "timestamp": revision.created_at,
+            "user_display_name": user_display_name(revision.user),
+        }
+
     def form_valid(self, form):
         self.form = form
         with transaction.atomic():
@@ -420,6 +459,12 @@ class EditView(
         if context["can_delete"]:
             context["delete_url"] = self.get_delete_url()
             context["delete_item_label"] = self.delete_item_label
+
+        context["revision_enabled"] = self.revision_enabled
+        context["draftstate_enabled"] = self.draftstate_enabled
+
+        context["live_last_updated_info"] = self.get_live_last_updated_info()
+        context["draft_last_updated_info"] = self.get_draft_last_updated_info()
         return context
 
 

+ 43 - 0
wagtail/snippets/side_panels.py

@@ -0,0 +1,43 @@
+from wagtail.admin.ui.side_panels import BaseSidePanels, BaseStatusSidePanel
+
+
+class SnippetStatusSidePanel(BaseStatusSidePanel):
+    def get_status_templates(self, context):
+        templates = []
+
+        if self.object.pk:
+            templates += [
+                "wagtailsnippets/snippets/side_panels/includes/status/workflow.html",
+            ]
+
+        if context.get("locale"):
+            templates += ["wagtailadmin/shared/side_panels/includes/status/locale.html"]
+
+        return templates
+
+    def get_context_data(self, parent_context):
+        context = super().get_context_data(parent_context)
+        inherit = [
+            "view",
+            "revision_enabled",
+            "draftstate_enabled",
+            "live_last_updated_info",
+            "draft_last_updated_info",
+            "locale",
+            "translations",
+        ]
+        context.update({k: parent_context.get(k) for k in inherit})
+
+        translations = context.get("translations")
+        if translations:
+            context["translations_total"] = len(context["translations"]) + 1
+
+        context["status_templates"] = self.get_status_templates(context)
+        return context
+
+
+class SnippetSidePanels(BaseSidePanels):
+    def __init__(self, request, object):
+        self.side_panels = [
+            SnippetStatusSidePanel(object, request),
+        ]

+ 7 - 1
wagtail/snippets/templates/wagtailsnippets/snippets/create.html

@@ -1,9 +1,15 @@
 {% extends "wagtailadmin/base.html" %}
 {% load i18n wagtailadmin_tags %}
 {% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}New  {{ snippet_type_name }}{% endblocktrans %}{% endblock %}
+{% block bodyclass %}page-editor create model-{{ model_opts.model_name }}{% endblock %}
 {% block content %}
+    <div class="w-sticky w-top-0 w-z-header">
+        {% include 'wagtailsnippets/snippets/headers/create_header.html' %}
+        {% include "wagtailadmin/shared/side_panels.html" %}
+    </div>
+
     {% trans "New" as new_str %}
-    {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" merged=1 locale=locale translations=translations only %}
+    {% include "wagtailadmin/shared/header.html" with title=new_str subtitle=model_opts.verbose_name icon="snippet" merged=1 only %}
 
     <form action="{{ action_url }}" method="POST" novalidate{% if form.is_multipart %} enctype="multipart/form-data"{% endif %}>
         {% csrf_token %}

+ 9 - 4
wagtail/snippets/templates/wagtailsnippets/snippets/edit.html

@@ -1,9 +1,14 @@
 {% extends "wagtailadmin/base.html" %}
 {% load wagtailadmin_tags i18n %}
-{% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}Editing {{ snippet_type_name }} - {{ instance }}{% endblocktrans %}{% endblock %}
+{% block titletag %}{% blocktrans trimmed with snippet_type_name=model_opts.verbose_name %}Editing {{ snippet_type_name }} - {{ object }}{% endblocktrans %}{% endblock %}
+{% block bodyclass %}page-editor model-{{ model_opts.model_name }} {% endblock %}
 {% block content %}
-    {% trans "Editing" as editing_str %}
-    {% include "wagtailsnippets/snippets/_header_with_history.html" with title=editing_str subtitle=instance icon="snippet" merged=1 locale=locale translations=translations latest_log_entry=latest_log_entry history_url=history_url model_opts=model_opts instance=instance only %}
+    <div class="w-sticky w-top-0 w-z-header">
+        {% include 'wagtailsnippets/snippets/headers/edit_header.html' %}
+        {% include "wagtailadmin/shared/side_panels.html" %}
+    </div>
+
+    {% include "wagtailadmin/shared/header.html" with title=object icon="snippet" merged=1 only %}
 
     <div class="row row-flush">
 
@@ -31,7 +36,7 @@
                 <dl>
                     <dt>{% trans "Usage" %}</dt>
                     <dd>
-                        <a href="{{ instance.usage_url }}">{% blocktrans trimmed count usage_count=instance.get_usage.count %}Used {{ usage_count }} time{% plural %}Used {{ usage_count }} times{% endblocktrans %}</a>
+                        <a href="{{ object.usage_url }}">{% blocktrans trimmed count usage_count=object.get_usage.count %}Used {{ usage_count }} time{% plural %}Used {{ usage_count }} times{% endblocktrans %}</a>
                     </dd>
                 </dl>
             </div>

+ 43 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/_base_header.html

@@ -0,0 +1,43 @@
+{% extends 'wagtailadmin/shared/headers/slim_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{# TODO: Replace this with wagtailadmin/shared/breadcrumbs.html once it's possible #}
+
+{% block header_content %}
+    {% with breadcrumb_link_classes='w-flex w-items-center w-h-full w-text-primary w-px-0.5 w-no-underline w-outline-offset-inside hover:w-underline hover:w-text-primary 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 w-text-14' 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" data-breadcrumb-next>
+            <button
+                type="button"
+                data-toggle-breadcrumbs
+                class="w-flex w-items-center w-justify-center w-box-border w-ml-0 w-p-4 w-w-[50px] w-h-full w-bg-transparent w-text-grey-400 w-transition hover:w-scale-110 hover:w-text-primary w-outline-offset-inside"
+                aria-label="{% trans 'Toggle breadcrumbs' %}"
+                aria-expanded="false"
+            >
+                {% icon name="breadcrumb-expand" class_name="w-w-4 w-h-4" %}
+            </button>
+
+            <div class="w-relative w-h-[50px] 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">
+                        {% block breadcrumb_items %}
+                            <li class="{{ breadcrumb_item_classes }} w-max-w-0" data-breadcrumb-item hidden>
+                                <a class="{{ breadcrumb_link_classes }}" href="{% url 'wagtailsnippets:index' %}">
+                                    {% trans "Snippets" %}
+                                </a>
+                                {% icon name="arrow-right" class_name=icon_classes %}
+                            </li>
+                            <li class="{{ breadcrumb_item_classes }} w-max-w-0" data-breadcrumb-item hidden>
+                                <a class="{{ breadcrumb_link_classes }}" href="{% url view.index_url_name %}">
+                                    {{ model_opts.verbose_name_plural|capfirst }}
+                                </a>
+                                {% icon name="arrow-right" class_name=icon_classes %}
+                            </li>
+                        {% endblock %}
+                    </ol>
+                </nav>
+            </div>
+        </div>
+    {% endwith %}
+{% endblock %}

+ 28 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/create_header.html

@@ -0,0 +1,28 @@
+{% extends 'wagtailsnippets/snippets/headers/_base_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+    <li class="{{ breadcrumb_item_classes }}">
+        <div class="w-flex w-justify-start w-items-center">
+            {{ title }}
+        </div>
+    </li>
+{% endblock %}
+
+{% block header_content %}
+    {% with model_opts.verbose_name|capfirst as model_name %}
+        {% trans 'New: '|add:model_name as title %}
+        {{ block.super }}
+
+        <h1 class="w-sr-only">
+            {{ title }}
+        </h1>
+    {% endwith %}
+{% endblock %}
+
+{% block actions %}
+    {% with nav_icon_classes='w-w-4 w-h-4' nav_icon_button_classes='w-h-[50px] w-bg-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-grey-400 w-transition hover:w-transform hover:w-scale-110 hover:w-text-primary focus:w-text-primary' %}
+        {% include "wagtailadmin/shared/side_panel_toggles.html" %}
+    {% endwith %}
+{% endblock %}

+ 36 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/edit_header.html

@@ -0,0 +1,36 @@
+{% extends 'wagtailsnippets/snippets/headers/_base_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+    <li class="{{ breadcrumb_item_classes }}">
+        <a class="{{ breadcrumb_link_classes }}" href="{% url view.edit_url_name object.pk|admin_urlquote %}">
+            {{ object }}
+        </a>
+    </li>
+{% endblock %}
+
+{% block header_content %}
+    {{ block.super }}
+
+    <h1 class="w-sr-only">
+        {% blocktrans trimmed with title=object snippet_type=model_opts.verbose_name %}Editing {{ snippet_type }} {{ title }}{% endblocktrans %}
+    </h1>
+{% endblock %}
+
+{% block actions %}
+    {% with nav_icon_classes='w-w-4 w-h-4' nav_icon_button_classes='w-h-[50px] w-bg-transparent w-box-border w-py-3 w-px-3 w-flex w-justify-center w-items-center w-outline-offset-inside w-text-grey-400 w-transition hover:w-transform hover:w-scale-110 hover:w-text-primary focus:w-text-primary' %}
+        {% include "wagtailadmin/shared/side_panel_toggles.html" %}
+
+        {# Object history #}
+        <a href="{{ history_url }}"
+            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" class_name=nav_icon_classes %}
+        </a>
+    {% endwith %}
+{% endblock %}

+ 17 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/history_header.html

@@ -0,0 +1,17 @@
+{% extends 'wagtailsnippets/snippets/headers/_base_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+    <li class="{{ breadcrumb_item_classes }} w-max-w-0" data-breadcrumb-item hidden>
+        <a class="{{ breadcrumb_link_classes }}" href="{% url view.edit_url_name object.pk|admin_urlquote %}">
+            {{ object }}
+        </a>
+        {% icon name="arrow-right" class_name=icon_classes %}
+    </li>
+    <li class="{{ breadcrumb_item_classes }}">
+        <div class="w-flex w-justify-start w-items-center">
+            {% trans "History" %}
+        </div>
+    </li>
+{% endblock %}

+ 16 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/list_header.html

@@ -0,0 +1,16 @@
+{% extends 'wagtailsnippets/snippets/headers/_base_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% block breadcrumb_items %}
+    <li class="{{ breadcrumb_item_classes }} w-max-w-0" data-breadcrumb-item hidden>
+        <a class="{{ breadcrumb_link_classes }}" href="{% url 'wagtailsnippets:index' %}">
+            {% trans "Snippets" %}
+        </a>
+        {% icon name="arrow-right" class_name=icon_classes %}
+    </li>
+    <li class="{{ breadcrumb_item_classes }}">
+        <a class="{{ breadcrumb_link_classes }}" href="{% url view.index_url_name %}">
+            {{ model_opts.verbose_name_plural|capfirst }}
+        </a>
+    </li>
+{% endblock breadcrumb_items %}

+ 17 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/headers/usage_header.html

@@ -0,0 +1,17 @@
+{% extends 'wagtailsnippets/snippets/headers/_base_header.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% block breadcrumb_items %}
+    {{ block.super }}
+    <li class="{{ breadcrumb_item_classes }} w-max-w-0" data-breadcrumb-item hidden>
+        <a class="{{ breadcrumb_link_classes }}" href="{% url view.edit_url_name object.pk|admin_urlquote %}">
+            {{ object }}
+        </a>
+        {% icon name="arrow-right" class_name=icon_classes %}
+    </li>
+    <li class="{{ breadcrumb_item_classes }}">
+        <div class="w-flex w-justify-start w-items-center">
+            {% trans "Usage" %}
+        </div>
+    </li>
+{% endblock %}

+ 7 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/history.html

@@ -2,11 +2,18 @@
 {% load i18n wagtailadmin_tags %}
 
 {% block titletag %}{% blocktrans trimmed with title=subtitle %}Snippet history for {{ subtitle }}{% endblocktrans %}{% endblock %}
+{% block bodyclass %}model-{{ model_opts.model_name }}{% endblock %}
 
 {% block actions %}
     <a href="{% url view.edit_url_name object.pk|admin_urlquote %}" class="button bicolor icon icon-edit">{% trans "Edit this snippet" %}</a>
 {% endblock %}
 
+{% block content %}
+    {% include 'wagtailsnippets/snippets/headers/history_header.html' %}
+
+    {{ block.super }}
+{% endblock %}
+
 {% block results %}
     {% if object_list %}
         {% component table %}

+ 61 - 0
wagtail/snippets/templates/wagtailsnippets/snippets/side_panels/includes/status/workflow.html

@@ -0,0 +1,61 @@
+{% extends 'wagtailadmin/shared/side_panels/includes/action_list_item.html' %}
+{% load wagtailadmin_tags i18n %}
+
+{% comment %}
+    This template is used to show Live, Draft, Live and Draft, In Moderation or Live and In Moderation.
+    Sometimes {{ block.super }} will be called two times in the instances where the object is in multiple states eg. Live + Draft
+{% endcomment %}
+
+{% block content %}
+    <div class="w-space-y-3">
+        {% trans 'Status: ' as screen_reader_title_prefix %}
+
+        {# Live section #}
+        {% if live_last_updated_info %}
+            {% trans 'Live' as title %}
+            {% with icon_name='snippet' timestamp=live_last_updated_info.timestamp user_display_name=live_last_updated_info.user_display_name %}
+                {% timesince_last_update timestamp user_display_name=user_display_name use_shorthand=True as help_text %}
+                {% if draft_last_updated_info %}
+                    {% with hide_action=True %}
+                        {{ block.super }}
+                    {% endwith %}
+                {% else %}
+                    {{ block.super }}
+                {% endif %}
+            {% endwith %}
+        {% endif %}
+
+        {# Draft section #}
+        {% if draft_last_updated_info %}
+            {% trans 'Draft' as title %}
+            {% timesince_last_update draft_last_updated_info.timestamp user_display_name=draft_last_updated_info.user_display_name use_shorthand=True as help_text %}
+
+            {# Icon #}
+            {% with icon_name='draft' %}
+                {{ block.super }}
+            {% endwith %}
+        {% endif %}
+    </div>
+{% endblock %}
+
+{% block action %}
+    {% with action_url=view.get_history_url %}
+        {% trans 'View history' as action_text %}
+        {{ block.super }}
+    {% endwith %}
+{% endblock %}
+
+{% block bottom %}
+    {# Workflow Status #}
+    {% with latest_revision=object.get_latest_revision %}
+        {# Scheduled publishing #}
+        {% if draftstate_enabled and latest_revision and latest_revision.approved_go_live_at %}
+            <div class="w-flex w-space-x-3">
+                {% icon name='info-circle' class_name='w-w-4 w-h-4 w-text-info-100 w-shrink-0' %}
+                <div class="w-label-3 w-flex-1">
+                    {% trans 'This will publish at ' %}{{ latest_revision.approved_go_live_at }}
+                </div>
+            </div>
+        {% endif %}
+    {% endwith %}
+{% endblock %}

+ 4 - 4
wagtail/snippets/templates/wagtailsnippets/snippets/type_index.html

@@ -1,9 +1,9 @@
 {% extends "wagtailadmin/base.html" %}
 {% load i18n wagtailadmin_tags %}
 {% block titletag %}{% blocktrans trimmed with snippet_type_name_plural=model_opts.verbose_name_plural|capfirst %}Snippets {{ snippet_type_name_plural }}{% endblocktrans %}{% endblock %}
+{% block bodyclass %}model-{{ model_opts.model_name }}{% endblock %}
 
 {% block extra_js %}
-    {{ block.super }}
     <script>
         window.headerSearch = {
             url: "{% url view.index_results_url_name %}",
@@ -17,17 +17,17 @@
 {% endblock %}
 
 {% block content %}
+    {% include 'wagtailsnippets/snippets/headers/list_header.html' %}
 
     <header class="header">
         <div class="row">
             <div class="left">
                 <div class="col">
                     <h1 class="header__title">
-                        {% icon name="snippet" %}
-                        {% blocktrans trimmed with snippet_type_name_plural=model_opts.verbose_name_plural|capfirst %}Snippets <span>{{ snippet_type_name_plural }}</span>{% endblocktrans %}
+                        {% icon name="snippet" %} {{ model_opts.verbose_name_plural|capfirst }}
                     </h1>
                     {% if is_searchable and search_url %}
-                        <form class="col search-form" action="{{ search_url }}" method="get" novalidate>
+                        <form class="col search-form" action="{% url search_url %}" method="get" novalidate>
                             <ul class="fields">
                                 {% for field in search_form %}
                                     {% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" icon="search" %}

+ 7 - 3
wagtail/snippets/templates/wagtailsnippets/snippets/usage.html

@@ -1,9 +1,13 @@
 {% extends "wagtailadmin/base.html" %}
-{% load i18n %}
-{% block titletag %}{% blocktrans trimmed with title=instance %}Usage of {{ title }}{% endblocktrans %}{% endblock %}
+{% load i18n wagtailadmin_tags %}
+{% block titletag %}{% blocktrans trimmed with title=object %}Usage of {{ title }}{% endblocktrans %}{% endblock %}
+{% block bodyclass %}model-{{ model_opts.model_name }}{% endblock %}
+
 {% block content %}
+    {% include 'wagtailsnippets/snippets/headers/usage_header.html' %}
+
     {% trans "Usage of" as usage_str %}
-    {% include "wagtailadmin/shared/header.html" with title=usage_str subtitle=instance %}
+    {% include "wagtailadmin/shared/header.html" with title=usage_str subtitle=object %}
 
     <div class="nice-padding">
         <table class="listing">

+ 15 - 66
wagtail/snippets/tests.py

@@ -600,14 +600,7 @@ class TestLocaleSelectorOnCreate(TestCase, WagtailTestUtils):
             reverse("wagtailsnippets_snippetstests_translatablesnippet:add")
         )
 
-        switch_to_french_url = (
-            reverse("wagtailsnippets_snippetstests_translatablesnippet:add")
-            + "?locale=fr"
-        )
-        self.assertContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertContains(response, "Switch locales")
 
     @override_settings(WAGTAIL_I18N_ENABLED=False)
     def test_locale_selector_not_present_when_i18n_disabled(self):
@@ -615,25 +608,12 @@ class TestLocaleSelectorOnCreate(TestCase, WagtailTestUtils):
             reverse("wagtailsnippets_snippetstests_translatablesnippet:add")
         )
 
-        switch_to_french_url = (
-            reverse("wagtailsnippets_snippetstests_translatablesnippet:add")
-            + "?locale=fr"
-        )
-        self.assertNotContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertNotContains(response, "Switch locales")
 
     def test_locale_selector_not_present_on_non_translatable_snippet(self):
         response = self.client.get(reverse("wagtailsnippets_tests_advert:add"))
 
-        switch_to_french_url = (
-            reverse("wagtailsnippets_tests_advert:add") + "?locale=fr"
-        )
-        self.assertNotContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertNotContains(response, "Switch locales")
 
 
 class BaseTestSnippetEditView(TestCase, WagtailTestUtils):
@@ -692,10 +672,6 @@ class TestSnippetEditView(BaseTestSnippetEditView):
         self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html")
         self.assertNotContains(response, 'role="tablist"')
 
-        # "Last updated" timestamp should be present
-        self.assertContains(
-            response, 'data-wagtail-tooltip="Sept. 30, 2021, 10:01 a.m."'
-        )
         # History link should be present
         self.assertContains(
             response,
@@ -928,8 +904,8 @@ class TestEditFileUploadSnippet(BaseTestSnippetEditView):
 class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
     fixtures = ["test.json"]
 
-    LOCALE_SELECTOR_HTML = '<a href="javascript:void(0)" aria-label="English" class="c-dropdown__button u-btn-current w-no-underline">'
-    LOCALE_INDICATOR_HTML = '<use href="#icon-site"></use></svg>\n    English'
+    LOCALE_SELECTOR_LABEL = "Switch locales"
+    LOCALE_INDICATOR_HTML = '<h3 id="status-sidebar-english"'
 
     def setUp(self):
         super().setUp()
@@ -940,56 +916,29 @@ class TestLocaleSelectorOnEdit(BaseTestSnippetEditView):
 
     def test_locale_selector(self):
         response = self.get()
-
-        self.assertContains(response, self.LOCALE_SELECTOR_HTML)
-
-        switch_to_french_url = reverse(
-            "wagtailsnippets_snippetstests_translatablesnippet:edit",
-            args=[quote(self.test_snippet_fr.pk)],
-        )
-        self.assertContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertContains(response, self.LOCALE_SELECTOR_LABEL)
+        self.assertContains(response, self.LOCALE_INDICATOR_HTML)
 
     def test_locale_selector_without_translation(self):
         self.test_snippet_fr.delete()
-
         response = self.get()
-
+        # The "Switch locale" button should not be shown
+        self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
+        # Locale status still available and says "No other translations"
         self.assertContains(response, self.LOCALE_INDICATOR_HTML)
-
-        switch_to_french_url = reverse(
-            "wagtailsnippets_snippetstests_translatablesnippet:edit",
-            args=[quote(self.test_snippet_fr.pk)],
-        )
-        self.assertNotContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertContains(response, "No other translations")
 
     @override_settings(WAGTAIL_I18N_ENABLED=False)
     def test_locale_selector_not_present_when_i18n_disabled(self):
         response = self.get()
-
-        self.assertNotContains(response, self.LOCALE_SELECTOR_HTML)
-
-        switch_to_french_url = reverse(
-            "wagtailsnippets_snippetstests_translatablesnippet:edit",
-            args=[quote(self.test_snippet_fr.pk)],
-        )
-        self.assertNotContains(
-            response,
-            f'<a href="{switch_to_french_url}" aria-label="French" class="u-link is-live w-no-underline">',
-        )
+        self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
+        self.assertNotContains(response, self.LOCALE_INDICATOR_HTML)
 
     def test_locale_selector_not_present_on_non_translatable_snippet(self):
         self.test_snippet = Advert.objects.get(pk=1)
-
         response = self.get()
-
-        self.assertNotContains(response, self.LOCALE_SELECTOR_HTML)
-        self.assertNotContains(response, 'aria-label="French" class="u-link is-live">')
+        self.assertNotContains(response, self.LOCALE_SELECTOR_LABEL)
+        self.assertNotContains(response, self.LOCALE_INDICATOR_HTML)
 
 
 class TestEditRevisionSnippet(BaseTestSnippetEditView):

+ 28 - 14
wagtail/snippets/views/snippets.py

@@ -34,6 +34,7 @@ from wagtail.search.backends import get_search_backend
 from wagtail.snippets.action_menu import SnippetActionMenu
 from wagtail.snippets.models import get_snippet_models
 from wagtail.snippets.permissions import user_can_edit_snippet_type
+from wagtail.snippets.side_panels import SnippetSidePanels
 from wagtail.utils.deprecation import RemovedInWagtail50Warning
 
 
@@ -236,14 +237,20 @@ class Create(CreateView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
-        media = context.get("media")
         action_menu = self._get_action_menu()
+        media = context.get("media") + action_menu.media
+
+        side_panels = None
+        if self.locale:
+            side_panels = SnippetSidePanels(self.request, self.model())
+            media += side_panels.media
 
         context.update(
             {
                 "model_opts": self.model._meta,
                 "action_menu": action_menu,
-                "media": media + action_menu.media,
+                "side_panels": side_panels,
+                "media": media,
             }
         )
 
@@ -329,9 +336,6 @@ class Edit(EditView):
             self.request, view=self.view_name, instance=self.object
         )
 
-    def _get_latest_log_entry(self):
-        return log_registry.get_logs_for_instance(self.object).first()
-
     def get_form_kwargs(self):
         return {**super().get_form_kwargs(), "for_user": self.request.user}
 
@@ -340,16 +344,14 @@ class Edit(EditView):
 
         media = context.get("media")
         action_menu = self._get_action_menu()
-        latest_log_entry = self._get_latest_log_entry()
+        side_panels = SnippetSidePanels(self.request, self.object)
 
         context.update(
             {
                 "model_opts": self.model._meta,
-                "instance": self.object,
                 "action_menu": action_menu,
-                "latest_log_entry": latest_log_entry,
-                "history_url": self.get_history_url(),
-                "media": media + action_menu.media,
+                "side_panels": side_panels,
+                "media": media + action_menu.media + side_panels.media,
             }
         )
 
@@ -455,17 +457,18 @@ class Usage(IndexView):
     template_name = "wagtailsnippets/snippets/usage.html"
     paginate_by = 20
     page_kwarg = "p"
+    is_searchable = False
 
     def setup(self, request, *args, pk, **kwargs):
         super().setup(request, *args, **kwargs)
         self.pk = pk
-        self.instance = self._get_instance()
+        self.object = self.get_object()
 
-    def _get_instance(self):
+    def get_object(self):
         return get_object_or_404(self.model, pk=unquote(self.pk))
 
     def get_queryset(self):
-        return self.instance.get_usage()
+        return self.object.get_usage()
 
     def paginate_queryset(self, queryset, page_size):
         paginator = self.get_paginator(
@@ -481,7 +484,13 @@ class Usage(IndexView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context.update({"instance": self.instance, "used_by": context.get("page_obj")})
+        context.update(
+            {
+                "object": self.object,
+                "used_by": context.get("page_obj"),
+                "model_opts": self.model._meta,
+            }
+        )
         return context
 
 
@@ -536,6 +545,7 @@ class ActionColumn(Column):
 
 class History(ReportView):
     view_name = "history"
+    index_url_name = None
     edit_url_name = None
     revisions_revert_url_name = None
     revisions_compare_url_name = None
@@ -565,6 +575,7 @@ class History(ReportView):
         context = super().get_context_data(*args, object_list=object_list, **kwargs)
         context["object"] = self.object
         context["subtitle"] = self.get_page_subtitle()
+        context["model_opts"] = self.model._meta
         return context
 
     def get_queryset(self):
@@ -672,6 +683,8 @@ class SnippetViewSet(ViewSet):
         return self.usage_view_class.as_view(
             model=self.model,
             permission_policy=self.permission_policy,
+            index_url_name=self.get_url_name("list"),
+            edit_url_name=self.get_url_name("edit"),
         )
 
     @property
@@ -679,6 +692,7 @@ class SnippetViewSet(ViewSet):
         return self.history_view_class.as_view(
             model=self.model,
             permission_policy=self.permission_policy,
+            index_url_name=self.get_url_name("list"),
             edit_url_name=self.get_url_name("edit"),
             revisions_revert_url_name=self.get_url_name("revisions_revert"),
             revisions_compare_url_name=self.get_url_name("revisions_compare"),