-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
List View for Image Listing #13172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
List View for Image Listing #13172
Conversation
@benenright Two new icons are required for the button to toggle to list view and grid view. Design decision for the listing is also required here. |
06d6452
to
dc81498
Compare
<th>{% trans "Collection" %}</th> | ||
<th> | ||
{% if ordering == "-created_at" %} | ||
<a href="{% querystring ordering='created_at' %}" title="Sort by 'Created' in descending order." class="icon icon-arrow-down-after teal label" >{% trans "Created" %}</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{% translate "Sort by 'Created' in descending order." %}
Other text based attributes too. ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would probably be fixed if we use Wagtail tables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 note from Sage this will likely be fixed by switching to the tables / columns framework we use elsewhere in Wagtail. @joelwilliam2005 if that’s not fully taken care of with that refactoring, please take a look at other sorting labels around the admin and try to reuse them, so we avoid creating extra translations.
{% if ordering == "-created_at" %} | ||
<a href="{% querystring ordering='created_at' %}" title="Sort by 'Created' in descending order." class="icon icon-arrow-down-after teal label" >{% trans "Created" %}</a> | ||
{% elif ordering == "created_at" %} | ||
<a href="{% querystring ordering='-created_at' %}" title="Sort by 'Created' in ascending order." class="icon icon-arrow-up-after teal label" >{% trans "Created" %}</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New preferred way is to write this out {% translate ...
https://docs.djangoproject.com/en/5.2/topics/i18n/translation/#translate-template-tag
{% block list_view %} | ||
{% panel id="listings" %} | ||
|
||
<table class="listing"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From Sage: this should be refactored to use Wagtail’s table framework (see document listing), where table columns are defined as Python objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. You'll need to define a columns
property on the view class. Here's an example for the documents listing view:
wagtail/wagtail/documents/views/documents.py
Lines 91 to 115 in 3208a1e
@cached_property | |
def columns(self): | |
columns = [ | |
BulkActionsColumn("bulk_actions"), | |
TitleColumn( | |
"title", | |
label=_("Title"), | |
sort_key="title", | |
get_url=self.get_edit_url, | |
get_title_id=lambda doc: f"document_{quote(doc.pk)}_title", | |
), | |
DownloadColumn("filename", label=_("File")), | |
DateColumn( | |
"created_at", | |
label=_("Created"), | |
sort_key="created_at", | |
width="16%", | |
), | |
] | |
if self.filters and "collection_id" in self.filters.filters: | |
columns.insert( | |
3, | |
Column("collection", label=_("Collection"), accessor="collection.name"), | |
) | |
return columns |
You might need to return an empty list if view_mode
is grid
. Maybe extract it into a cached property on the view class so you can use it here e.g.
if self.view_mode == "grid":
return []
You can create custom Column
subclasses if you need to make customisations. For example, the UserColumn
on the users listing view was created so that it can display the user's avatar along with their name:
wagtail/wagtail/users/views/users.py
Lines 58 to 59 in 3208a1e
class UserColumn(TitleColumn): | |
cell_template_name = "wagtailusers/users/user_cell.html" |
{% block title %} | |
<div class="w-inline-flex w-items-center"> | |
{% avatar user=instance size="small" classname="w-shrink-0" %} | |
{{ block.super }} | |
</div> | |
{% endblock %} |
I think you'll need to create the following custom columns:
- A custom
BulkActionsCheckboxColumn
subclass for images. See the example for documents:wagtail/wagtail/documents/views/documents.py
Lines 32 to 41 in 3208a1e
class BulkActionsColumn(BulkActionsCheckboxColumn): def __init__(self, *args, **kwargs): super().__init__(*args, obj_type="document", **kwargs) def get_header_context_data(self, parent_context): context = super().get_header_context_data(parent_context) parent = parent_context.get("current_collection") if parent: context["parent"] = parent.id return context - A new
ImageColumn
to render the image as a separate column - A custom
TitleColumn
subclass in order to display the image file name along with the title.
The main code for the tables framework can be found at https://github.com/wagtail/wagtail/blob/main/wagtail/admin/ui/tables/__init__.py
Once you've set up the columns, you should be able to do something like
{% if view_mode == "list" %}
{{ block.super }}
{% else %}
{# Keep the current code #}
{% endif %}
<th>{% trans "Preview" %}</th> | ||
<th> | ||
{% if ordering == "-title" %} | ||
<a href="{% querystring ordering='title' %}" title="Sort by 'Title' in descending order." class="icon icon-arrow-down-after teal label" >{% trans "Title" %}</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likely not relevant (see above) – if we keep this, we’d need to use Wagtail’s own implementation of querystring
rather than Django’s, as we still have to support Django 4.2
wagtail/images/views/images.py
Outdated
context.update( | ||
{ | ||
"next": self.get_next_url(), | ||
"current_collection": self.current_collection, | ||
"current_ordering": self.ordering, | ||
"ORDERING_OPTIONS": self.ORDERING_OPTIONS, | ||
"view_mode": view_mode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need tests that confirm:
- the view mode is passed correctly
- the listings’ HTML are adapted based on the view mode
From Sage: need tests that would fail if they ran against the previous implementation of those listings (so demonstrate the changes add this new behavior).
Assert contents of title column with title + filename (below title), matching the test’s created image
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test client is a Python class that acts as a dummy web browser, allowing you to test your views and interact with your Django-powered application programmatically. It comes with html asserts, on of them:
https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.SimpleTestCase.assertInHTML
The WagtailTestCase also has a BeautifulSoup helper:
utils.wagtail_tests.WagtailTestUtils.get_soup
.
BS is an awesome package to parse HTML and validate its contents: https://www.crummy.com/software/BeautifulSoup/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of testing the markup:
wagtail/wagtail/admin/tests/viewsets/test_model_viewset.py
Lines 1190 to 1224 in 3208a1e
def test_simple(self): | |
response = self.client.get(self.url) | |
self.assertEqual(response.status_code, 200) | |
soup = self.get_soup(response.content) | |
h1 = soup.select_one("h1") | |
self.assertEqual(h1.text.strip(), f"Usage: {self.object}") | |
tds = soup.select("#listing-results tbody tr td") | |
self.assertEqual(len(tds), 3) | |
self.assertEqual(tds[0].text.strip(), str(self.tbx)) | |
self.assertEqual(tds[1].text.strip(), "Various on delete model") | |
self.assertEqual(tds[2].text.strip(), "Cascading toy") | |
tbx_edit_url = AdminURLFinder(self.user).get_edit_url(self.tbx) | |
# Link to referrer's edit view | |
link = tds[0].select_one("a") | |
self.assertIsNotNone(link) | |
self.assertEqual(link.attrs.get("href"), tbx_edit_url) | |
content_path_link = tds[-1].select_one("a") | |
self.assertEqual( | |
content_path_link.attrs.get("href"), | |
tbx_edit_url + "#:w:contentpath=cascading_toy", | |
) | |
# Link to referrer's edit view with parameters for the specific field | |
link = tds[2].select_one("a") | |
self.assertIsNotNone(link) | |
self.assertIn(tbx_edit_url, link.attrs.get("href")) | |
# Usage view is not searchable | |
input = soup.select_one("input#id_q") | |
self.assertIsNone(input) | |
self.assertFalse(response.context.get("search_form")) |
The tests should go in https://github.com/wagtail/wagtail/blob/main/wagtail/images/tests/test_admin_views.py
<a href="?view=grid"> | ||
<button | ||
type="button" | ||
class="w-filter-button" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to change that class name so it’s clearer it’s reused in multiple buttons.
type="button" | ||
class="w-filter-button" | ||
aria-label="{% trans 'Toggle view' %}" | ||
style="align-items: center; justify-content: center; display: flex" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need to be done with vanilla CSS or Tailwind utility classes, we can’t use inline styles.
<button | ||
type="button" | ||
class="w-filter-button" | ||
aria-label="{% trans 'Toggle view' %}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be switched to a tooltip like we have for the existing filter button. And changed the label to Toggle layout
.
{% include "wagtailadmin/bulk_actions/listing_checkbox_cell.html" with obj_type="image" instance=image aria_describedby=title_id only %} | ||
<td> | ||
<a href="{% url view.edit_url_name image.id %}{% if next %}?next={{ next|urlencode }}{% endif %}"> | ||
<div class="image">{% image image max-105x105 class="show-transparency" alt="" %}</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove alt=""
to get the default behavior of populating alt
from the image’s description or title.
<div class="image">{% image image max-105x105 class="show-transparency" alt="" %}</div> | |
<div class="image">{% image image max-105x105 class="show-transparency" %}</div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice progress!
{% block list_view %} | ||
{% panel id="listings" %} | ||
|
||
<table class="listing"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. You'll need to define a columns
property on the view class. Here's an example for the documents listing view:
wagtail/wagtail/documents/views/documents.py
Lines 91 to 115 in 3208a1e
@cached_property | |
def columns(self): | |
columns = [ | |
BulkActionsColumn("bulk_actions"), | |
TitleColumn( | |
"title", | |
label=_("Title"), | |
sort_key="title", | |
get_url=self.get_edit_url, | |
get_title_id=lambda doc: f"document_{quote(doc.pk)}_title", | |
), | |
DownloadColumn("filename", label=_("File")), | |
DateColumn( | |
"created_at", | |
label=_("Created"), | |
sort_key="created_at", | |
width="16%", | |
), | |
] | |
if self.filters and "collection_id" in self.filters.filters: | |
columns.insert( | |
3, | |
Column("collection", label=_("Collection"), accessor="collection.name"), | |
) | |
return columns |
You might need to return an empty list if view_mode
is grid
. Maybe extract it into a cached property on the view class so you can use it here e.g.
if self.view_mode == "grid":
return []
You can create custom Column
subclasses if you need to make customisations. For example, the UserColumn
on the users listing view was created so that it can display the user's avatar along with their name:
wagtail/wagtail/users/views/users.py
Lines 58 to 59 in 3208a1e
class UserColumn(TitleColumn): | |
cell_template_name = "wagtailusers/users/user_cell.html" |
{% block title %} | |
<div class="w-inline-flex w-items-center"> | |
{% avatar user=instance size="small" classname="w-shrink-0" %} | |
{{ block.super }} | |
</div> | |
{% endblock %} |
I think you'll need to create the following custom columns:
- A custom
BulkActionsCheckboxColumn
subclass for images. See the example for documents:wagtail/wagtail/documents/views/documents.py
Lines 32 to 41 in 3208a1e
class BulkActionsColumn(BulkActionsCheckboxColumn): def __init__(self, *args, **kwargs): super().__init__(*args, obj_type="document", **kwargs) def get_header_context_data(self, parent_context): context = super().get_header_context_data(parent_context) parent = parent_context.get("current_collection") if parent: context["parent"] = parent.id return context - A new
ImageColumn
to render the image as a separate column - A custom
TitleColumn
subclass in order to display the image file name along with the title.
The main code for the tables framework can be found at https://github.com/wagtail/wagtail/blob/main/wagtail/admin/ui/tables/__init__.py
Once you've set up the columns, you should be able to do something like
{% if view_mode == "list" %}
{{ block.super }}
{% else %}
{# Keep the current code #}
{% endif %}
wagtail/images/views/images.py
Outdated
@@ -111,12 +111,15 @@ def get_next_url(self): | |||
def get_context_data(self, **kwargs): | |||
context = super().get_context_data(**kwargs) | |||
|
|||
view_mode = self.request.GET.get("view", "grid") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably worth extracting into a cached property on the view, e.g.
@cached_property
def view_mode(self):
return self.request.GET.get("view", "grid")
So you can use it in other methods/properties as self.view_mode
, e.g. when you're defining the columns
.
And if we do want to preserve it in the session, we could do something like
@cached_property
def view_mode(self):
preferred_view = self.request.GET.get("view")
if preferred_view:
request.session["wagtailimages_view_mode"] = preferred_view
else:
preferred_view = request.session.get("wagtailimages_view_mode", "grid")
return preferred_view
@@ -110,6 +113,8 @@ <h1 class="w-sr-only"> | |||
required as the content is dynamically loaded. | |||
{% endcomment %} | |||
<input hidden disabled type="submit" /> | |||
<input type="hidden" name="view" value="{{ view_mode }}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned by Thibaud, try to see if we can put this in the extra_form_fields
instead.
And since this is specific to images, this should go in the image_listing_header.html
rather than the generic slim_header.html
wagtail/images/views/images.py
Outdated
context.update( | ||
{ | ||
"next": self.get_next_url(), | ||
"current_collection": self.current_collection, | ||
"current_ordering": self.ordering, | ||
"ORDERING_OPTIONS": self.ORDERING_OPTIONS, | ||
"view_mode": view_mode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of testing the markup:
wagtail/wagtail/admin/tests/viewsets/test_model_viewset.py
Lines 1190 to 1224 in 3208a1e
def test_simple(self): | |
response = self.client.get(self.url) | |
self.assertEqual(response.status_code, 200) | |
soup = self.get_soup(response.content) | |
h1 = soup.select_one("h1") | |
self.assertEqual(h1.text.strip(), f"Usage: {self.object}") | |
tds = soup.select("#listing-results tbody tr td") | |
self.assertEqual(len(tds), 3) | |
self.assertEqual(tds[0].text.strip(), str(self.tbx)) | |
self.assertEqual(tds[1].text.strip(), "Various on delete model") | |
self.assertEqual(tds[2].text.strip(), "Cascading toy") | |
tbx_edit_url = AdminURLFinder(self.user).get_edit_url(self.tbx) | |
# Link to referrer's edit view | |
link = tds[0].select_one("a") | |
self.assertIsNotNone(link) | |
self.assertEqual(link.attrs.get("href"), tbx_edit_url) | |
content_path_link = tds[-1].select_one("a") | |
self.assertEqual( | |
content_path_link.attrs.get("href"), | |
tbx_edit_url + "#:w:contentpath=cascading_toy", | |
) | |
# Link to referrer's edit view with parameters for the specific field | |
link = tds[2].select_one("a") | |
self.assertIsNotNone(link) | |
self.assertIn(tbx_edit_url, link.attrs.get("href")) | |
# Usage view is not searchable | |
input = soup.select_one("input#id_q") | |
self.assertIsNone(input) | |
self.assertFalse(response.context.get("search_form")) |
The tests should go in https://github.com/wagtail/wagtail/blob/main/wagtail/images/tests/test_admin_views.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple extra comments – looking solid @joelwilliam2005!
@@ -1,6 +1,10 @@ | |||
{% extends "wagtailadmin/shared/headers/slim_header.html" %} | |||
{% load wagtailadmin_tags %} | |||
|
|||
{% block view_toggle %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can use the pre-existing block which will place the button where expected.
{% block view_toggle %} | |
{% block extra_form_fields %} |
<th>{% trans "Collection" %}</th> | ||
<th> | ||
{% if ordering == "-created_at" %} | ||
<a href="{% querystring ordering='created_at' %}" title="Sort by 'Created' in descending order." class="icon icon-arrow-down-after teal label" >{% trans "Created" %}</a> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 note from Sage this will likely be fixed by switching to the tables / columns framework we use elsewhere in Wagtail. @joelwilliam2005 if that’s not fully taken care of with that refactoring, please take a look at other sorting labels around the admin and try to reuse them, so we avoid creating extra translations.
6cc1b1b
to
c434baf
Compare
{% load wagtailadmin_tags i18n %} | ||
|
||
<div id="view-toggle-button-wrapper"> | ||
{% if view_mode == "list" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest we change naming from view / view mode to "layout" (layout mode?)
client/scss/components/_button.scss
Outdated
@@ -365,3 +365,28 @@ | |||
color: theme('colors.text-link-hover'); | |||
} | |||
} | |||
|
|||
.w-view-toggle-button { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be reusing the same styles as w-filter-button
. Could you refactor both to use a generic name? Looks like those new w-view-toggle-button
styles could be applied to both.
{% block title %} | ||
<a class="image-choice" href="{% url 'wagtailimages:edit' instance.id %}{% if next %}?next={{ next|urlencode }}{% endif %}"> | ||
<figure> | ||
{% image instance max-105x105 class="show-transparency"%} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{% image instance max-105x105 class="show-transparency"%} | |
{% image instance max-165x165 class="show-transparency" %} |
Worthwhile for us to reuse the same rendition as everywhere else, for performance. Resize with CSS afterwards if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: need to double check what happens with tall vs. wide images. @thibaudcolas review other software with Ben, and alignment of text
<a class="image-choice" href="{% url 'wagtailimages:edit' instance.id %}{% if next %}?next={{ next|urlencode }}{% endif %}"> | ||
<figure> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove figure tag? And try to get the focus outline around the image to more closely follow the image’s size
type="button" | ||
class="w-view-toggle-button" | ||
data-controller="w-tooltip" | ||
data-w-tooltip-content-value="Toggle layout" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be translateable
</ul> | ||
</div> | ||
{% if view_mode == "list" %} | ||
{{block.super}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{{block.super}} | |
{{ block.super }} |
|
||
<div id="view-toggle-button-wrapper"> | ||
{% if view_mode == "list" %} | ||
<a href="?view=grid"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Copilot here, I think it'd be great if we could retain the search and ordering. Wagtail has its own implementation of the {% querystring %}
template tag to do this, which you can try to use e.g.
<a href="?view=grid"> | |
<a href="{{ index_url }}{% querystring layout="grid" %}"> |
That being said, this may not be necessary if we use the utilise the search/filter form to do this for us. See my other comment for details.
{% rawformattedfield label_text=_("Sort by") id_for_label="order_images_by" sr_only_label=True %} | ||
<select id="order_images_by" name="ordering"> | ||
{% for ordering, ordering_text in ORDERING_OPTIONS.items %} | ||
<option value="{{ ordering }}" {% if current_ordering == ordering %}selected="selected"{% endif %}>{{ ordering_text }}</option> | ||
{% endfor %} | ||
</select> | ||
{% endrawformattedfield %} | ||
<input type="hidden" name="view" value="{{ view_mode }}"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per my other comment, we could turn this into a proper checkbox input in the view_toggle_button.html
<input type="hidden" name="view" value="{{ view_mode }}"> |
<a href="?view=grid"> | ||
<button | ||
type="button" | ||
class="w-view-toggle-button" | ||
data-controller="w-tooltip" | ||
data-w-tooltip-content-value="Toggle layout" | ||
> | ||
{% icon name="grip" %} | ||
</button> | ||
</a> | ||
|
||
{% else %} | ||
<a href="?view=list"> | ||
<button | ||
type="button" | ||
class="w-view-toggle-button" | ||
data-controller="w-tooltip" | ||
data-w-tooltip-content-value="Toggle layout" | ||
> | ||
{% icon name="list-ul" %} | ||
</button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should turn this into a proper toggle input, i.e. a checkbox. This would allow us to piggyback on the Universal Listing's implementation of AJAX-based loading of the results instead of a full page reload. For example, I was able to get this working by using a hidden checkbox input and turning the button into a <label>
that wraps it. Try replacing the whole file with the following content:
{% load wagtailadmin_tags i18n %}
<label class="w-view-toggle-button w-cursor-pointer" data-controller="w-tooltip" data-w-tooltip-content-value="{% trans 'Toggle layout' %}">
{% if view_mode == "list" %}
{% icon name="grip" %}
{% else %}
{% icon name="list-ul" %}
{% endif %}
<input hidden type="checkbox" name="view" value="list" {% if view_mode == "list" %}checked{% endif %}>
</label>
(Note that the w-cursor-pointer
should probably be incorporated inside the styles for w-view-toggle-button
instead, so we don't have to use this Tailwind class separately.)
And then, you'll need to remove the existing hidden input in index.html
- <input type="hidden" name="view" value="{{ view_mode }}">
One hurdle is that we need to update the icon as well. This can be done with CSS, but it does mean we need something specific to this button being a toggle (rather than a generic "header button" CSS suggested by Thibaud). Alternatively, we can use JS with the TeleportController
, similar to how we do it for the filters.
CSS approach
Changing the icon with a CSS approach can be done like the following patch. You can save the following diff to a file e.g. ajax-toggle-css.diff
and do git apply ajax-toggle-css.diff
diff --git a/client/scss/components/_button.scss b/client/scss/components/_button.scss
index 875a1313ef..391fc67163 100644
--- a/client/scss/components/_button.scss
+++ b/client/scss/components/_button.scss
@@ -389,4 +389,20 @@
&:hover {
border-color: theme('colors.border-field-hover');
}
+
+ .w-view-toggle-button__icon--checked {
+ display: none;
+ }
+ .w-view-toggle-button__icon--unchecked {
+ display: block;
+ }
+
+ &:has(input:checked) {
+ .w-view-toggle-button__icon--checked {
+ display: block;
+ }
+ .w-view-toggle-button__icon--unchecked {
+ display: none;
+ }
+ }
}
diff --git a/wagtail/images/templates/wagtailimages/images/index.html b/wagtail/images/templates/wagtailimages/images/index.html
index b5d43ea5b0..c299f829a1 100644
--- a/wagtail/images/templates/wagtailimages/images/index.html
+++ b/wagtail/images/templates/wagtailimages/images/index.html
@@ -16,7 +16,6 @@
{% endfor %}
</select>
{% endrawformattedfield %}
- <input type="hidden" name="view" value="{{ view_mode }}">
{% endfragment %}
{% include "wagtailimages/images/image_listing_header.html" with breadcrumbs_items=breadcrumbs_items side_panels=side_panels history_url=history_url title=header_title search_url=index_results_url search_form=search_form filters=filters buttons=header_buttons icon_name=header_icon extra_form_fields=extra_form_fields only %}
{% endblock %}
diff --git a/wagtail/images/templates/wagtailimages/images/view_toggle_button.html b/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
index f6274a8c60..f71940f87c 100644
--- a/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
+++ b/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
@@ -1,28 +1,7 @@
{% load wagtailadmin_tags i18n %}
-<div id="view-toggle-button-wrapper">
- {% if view_mode == "list" %}
- <a href="?view=grid">
- <button
- type="button"
- class="w-view-toggle-button"
- data-controller="w-tooltip"
- data-w-tooltip-content-value="Toggle layout"
- >
- {% icon name="grip" %}
- </button>
- </a>
-
- {% else %}
- <a href="?view=list">
- <button
- type="button"
- class="w-view-toggle-button"
- data-controller="w-tooltip"
- data-w-tooltip-content-value="Toggle layout"
- >
- {% icon name="list-ul" %}
- </button>
- </a>
- {% endif %}
-</div>
+<label class="w-view-toggle-button w-cursor-pointer" data-controller="w-tooltip" data-w-tooltip-content-value="{% trans 'Toggle layout' %}">
+ {% icon name="grip" classname="w-view-toggle-button__icon--checked" %}
+ {% icon name="list-ul" classname="w-view-toggle-button__icon--unchecked" %}
+ <input hidden type="checkbox" name="view" value="list" {% if view_mode == "list" %}checked{% endif %}>
+</label>
JS approach
This is similar to how we handle any updates to the header elements (e.g. buttons, filter form) when the results are reloaded.
{% block before_results %} | |
{% if view.active_filters %} | |
{% include "wagtailadmin/shared/active_filters.html" with active_filters=view.active_filters %} | |
{% endif %} | |
{% if render_filters_fragment %} | |
<template data-controller="w-teleport" data-w-teleport-target-value="#filters-drilldown" data-w-teleport-reset-value="true"> | |
{% include "wagtailadmin/shared/headers/_filters.html" with filters=filters %} | |
</template> | |
{% endif %} | |
{% if render_buttons_fragment %} | |
<template data-controller="w-teleport" data-w-teleport-target-value="#w-slim-header-buttons" data-w-teleport-reset-value="true"> | |
{% for button in header_buttons %} | |
{% component button %} | |
{% endfor %} | |
</template> | |
{% endif %} |
Again, you can apply the following patch on top of your current branch to test it:
diff --git a/wagtail/images/templates/wagtailimages/images/index.html b/wagtail/images/templates/wagtailimages/images/index.html
index b5d43ea5b0..95672a3875 100644
--- a/wagtail/images/templates/wagtailimages/images/index.html
+++ b/wagtail/images/templates/wagtailimages/images/index.html
@@ -8,7 +8,9 @@
{% block slim_header %}
{% fragment as extra_form_fields %}
- {% include "wagtailimages/images/view_toggle_button.html" %}
+ <div id="view-toggle-button">
+ {% include "wagtailimages/images/view_toggle_button.html" %}
+ </div>
{% rawformattedfield label_text=_("Sort by") id_for_label="order_images_by" sr_only_label=True %}
<select id="order_images_by" name="ordering">
{% for ordering, ordering_text in ORDERING_OPTIONS.items %}
@@ -16,7 +18,6 @@
{% endfor %}
</select>
{% endrawformattedfield %}
- <input type="hidden" name="view" value="{{ view_mode }}">
{% endfragment %}
{% include "wagtailimages/images/image_listing_header.html" with breadcrumbs_items=breadcrumbs_items side_panels=side_panels history_url=history_url title=header_title search_url=index_results_url search_form=search_form filters=filters buttons=header_buttons icon_name=header_icon extra_form_fields=extra_form_fields only %}
{% endblock %}
diff --git a/wagtail/images/templates/wagtailimages/images/index_results.html b/wagtail/images/templates/wagtailimages/images/index_results.html
index 4a1c3c1cf0..5776e017d0 100644
--- a/wagtail/images/templates/wagtailimages/images/index_results.html
+++ b/wagtail/images/templates/wagtailimages/images/index_results.html
@@ -2,6 +2,14 @@
{% load wagtailimages_tags wagtailadmin_tags %}
{% load i18n l10n %}
+{% block before_results %}
+ {{ block.super }}
+
+ <template data-controller="w-teleport" data-w-teleport-target-value="#view-toggle-button" data-w-teleport-reset-value="true">
+ {% include "wagtailimages/images/view_toggle_button.html" %}
+ </template>
+{% endblock %}
+
{% block results %}
{% if view_mode == "list" %}
{{block.super}}
diff --git a/wagtail/images/templates/wagtailimages/images/view_toggle_button.html b/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
index f6274a8c60..98e88874ff 100644
--- a/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
+++ b/wagtail/images/templates/wagtailimages/images/view_toggle_button.html
@@ -1,28 +1,10 @@
{% load wagtailadmin_tags i18n %}
-<div id="view-toggle-button-wrapper">
+<label class="w-view-toggle-button w-cursor-pointer" data-controller="w-tooltip" data-w-tooltip-content-value="{% trans 'Toggle layout' %}">
{% if view_mode == "list" %}
- <a href="?view=grid">
- <button
- type="button"
- class="w-view-toggle-button"
- data-controller="w-tooltip"
- data-w-tooltip-content-value="Toggle layout"
- >
- {% icon name="grip" %}
- </button>
- </a>
-
+ {% icon name="grip" %}
{% else %}
- <a href="?view=list">
- <button
- type="button"
- class="w-view-toggle-button"
- data-controller="w-tooltip"
- data-w-tooltip-content-value="Toggle layout"
- >
- {% icon name="list-ul" %}
- </button>
- </a>
+ {% icon name="list-ul" %}
{% endif %}
-</div>
+ <input hidden type="checkbox" name="view" value="list" {% if view_mode == "list" %}checked{% endif %}>
+</label>
Feel free to play around with both approaches and let me know your thoughts. Also would love to hear your opinions @thibaudcolas and @allcaps.
b797990
to
fa2ab3c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! Tweaked the toggle button to fix the small accessibility issues we had discussed.
The icons also need very specific integration steps when creating new ones (see UI guideline docs).
fa2ab3c
to
a5cd658
Compare
This PR introduces a new List view for the image listing with a toggle button to switch between grid and list view.
Image Listing Grid View:

Image Listing List View:

Here's a demo video:
https://github.com/user-attachments/assets/03cfd8a7-433d-49c1-b9d9-b5cf8711fdf4
It is still not polished and requires work.
Design decisions required:
Just an opinion - I think that a separate column for file name would keep the design consistent and it looks better as well. However moving the filename below the title definetly leaves enough room for an extra column ;)
Here's how the listing would look with a separate column for file name.