-
Notifications
You must be signed in to change notification settings - Fork 170
Add support for bulk adding items to a list #1227
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
base: dev
Are you sure you want to change the base?
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,27 @@ | |
| {% block content %} | ||
| <h1 class="text-3xl font-bold mb-6">{{ media_type|media_type_readable_plural }}</h1> | ||
|
|
||
| <div x-data="{ sort: '{{ current_sort }}', status: '{{ current_status }}', layout: '{{ current_layout }}', search: '{{ request.GET.search|default:'' }}', statusLabels: { | ||
| <script> | ||
| document.addEventListener('alpine:init', () => { | ||
| Alpine.store('bulk', { | ||
| active: false, | ||
| ids: [], | ||
| toggle(id) { | ||
| const i = this.ids.indexOf(id); | ||
| i === -1 ? this.ids.push(id) : this.ids.splice(i, 1); | ||
| }, | ||
| selectAll() { | ||
| document.querySelectorAll('[data-bulk-id]').forEach(function(el) { | ||
| const id = parseInt(el.dataset.bulkId); | ||
| if (Alpine.store('bulk').ids.indexOf(id) === -1) Alpine.store('bulk').ids.push(id); | ||
| }); | ||
| }, | ||
|
Comment on lines
+21
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The A more performant approach would be to get all visible IDs, merge them with the already selected IDs using a |
||
| clear() { this.ids = []; this.active = false; }, | ||
| }); | ||
| }); | ||
| </script> | ||
|
|
||
| <div x-data="{ sort: '{{ current_sort }}', status: '{{ current_status }}', layout: '{{ current_layout }}', search: '{{ request.GET.search|default:'' }}', bulkListsOpen: false, statusLabels: { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| {% for value, label in status_choices %} | ||
| '{{ value }}': '{{ label }}' | ||
| {% if not forloop.last %},{% endif %} | ||
|
|
@@ -117,6 +137,16 @@ <h1 class="text-3xl font-bold mb-6">{{ media_type|media_type_readable_plural }}< | |
| </div> | ||
| </div> | ||
|
|
||
| <!-- Bulk Select Toggle --> | ||
| <button class="flex items-center px-4 py-2 rounded-md transition-colors cursor-pointer" | ||
| :class="$store.bulk.active ? 'bg-indigo-600 text-white' : 'bg-[#39404b] hover:bg-[#454d5a]'" | ||
| @click="$store.bulk.active = !$store.bulk.active; if (!$store.bulk.active) $store.bulk.ids = []" | ||
| type="button" | ||
| title="Select items"> | ||
| {% include "app/icons/circle-check.svg" with classes="w-4 h-4 mr-2" %} | ||
| Select | ||
| </button> | ||
|
|
||
| <!-- Layout Toggle --> | ||
| <div class="flex rounded-md overflow-hidden border border-gray-700"> | ||
| <a :href="`{% url 'medialist' media_type %}?${search ? 'search=' + search + '&' : ''}${status !== 'all' ? 'status=' + status + '&' : ''}${sort !== 'score' ? 'sort=' + sort + '&' : ''}layout=grid`" | ||
|
|
@@ -133,6 +163,47 @@ <h1 class="text-3xl font-bold mb-6">{{ media_type|media_type_readable_plural }}< | |
| </div> | ||
| </div> | ||
|
|
||
| <!-- Hidden form keeping selected IDs and CSRF token in sync for hx-include --> | ||
| <form id="bulk-ids-form" class="hidden"> | ||
| <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}"> | ||
| <template x-for="id in $store.bulk.ids" :key="id"> | ||
| <input type="hidden" name="item_ids" :value="id"> | ||
| </template> | ||
| </form> | ||
|
|
||
| <!-- Bulk action bar --> | ||
| <div x-show="$store.bulk.active" | ||
| x-cloak | ||
| class="sticky top-2 z-40 flex items-center gap-3 mb-4 p-3 bg-[#2a2f35] border border-gray-700 rounded-lg shadow-lg"> | ||
| <span class="text-sm text-gray-300"> | ||
| <span x-text="$store.bulk.ids.length"></span> selected | ||
| </span> | ||
| <button class="px-3 py-1.5 text-sm bg-[#39404b] hover:bg-[#454d5a] text-white rounded-md transition-colors cursor-pointer" | ||
| type="button" | ||
| @click="$store.bulk.selectAll()">Select all visible</button> | ||
| <button class="px-3 py-1.5 text-sm bg-emerald-600 hover:bg-emerald-500 text-white rounded-md transition-colors cursor-pointer" | ||
| type="button" | ||
| x-show="$store.bulk.ids.length > 0" | ||
| hx-get="{% url 'bulk_lists_modal' %}" | ||
| hx-target="#bulk-lists-modal-content" | ||
| hx-trigger="click" | ||
| @click="bulkListsOpen = true">Add to list</button> | ||
| <button class="ml-auto px-3 py-1.5 text-sm text-gray-400 hover:text-white transition-colors cursor-pointer" | ||
| type="button" | ||
| @click="$store.bulk.clear()">Cancel</button> | ||
| </div> | ||
|
|
||
| <!-- Bulk lists modal --> | ||
| <div x-show="bulkListsOpen" | ||
| x-cloak | ||
| @keydown.escape.window="bulkListsOpen = false" | ||
| class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> | ||
| <div class="w-96 max-h-[90vh] px-4 md:px-0 relative z-60" | ||
| @click.outside="bulkListsOpen = false"> | ||
| <div id="bulk-lists-modal-content"></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {% if not media_list %} | ||
| <div id="empty_list" | ||
| class="flex flex-col items-center justify-center py-16 bg-[#2a2f35] rounded-lg"> | ||
|
|
@@ -157,6 +228,7 @@ <h3 class="text-xl font-semibold mb-2">No {{ media_type|media_type_readable_plur | |
| <table class="w-full bg-[#2a2f35]"> | ||
| <thead class="text-left text-gray-400 text-sm"> | ||
| <tr> | ||
| <th x-show="$store.bulk.active" class="p-2 w-10"></th> | ||
| <th class="p-2 w-15"></th> | ||
| <th class="p-2 pe-8 w-2/5">Title</th> | ||
| <th class="p-2 text-center">Score</th> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
|
|
||
| <div class="bg-[#2a2f35] p-6 rounded-lg max-h-[70svh] overflow-y-auto"> | ||
| <div class="flex items-center justify-between mb-6"> | ||
| <h2 class="text-2xl font-bold text-white"> | ||
| Add <span x-text="$store.bulk.ids.length"></span> items to list | ||
| </h2> | ||
| <button class="text-gray-400 hover:text-white cursor-pointer" | ||
| @click="bulkListsOpen = false">{% include "app/icons/x.svg" with classes="w-6 h-6" %}</button> | ||
| </div> | ||
| <ul class="space-y-3"> | ||
| {% for custom_list in custom_lists %} | ||
| <li class="flex items-center justify-between bg-[#39404b] p-3 rounded-md gap-1"> | ||
| <span class="text-white">{{ custom_list.name }}</span> | ||
| <button class="px-3 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white text-sm rounded-md transition-colors cursor-pointer" | ||
| hx-post="{% url 'bulk_list_add' %}" | ||
| hx-include="#bulk-ids-form" | ||
| hx-vals='{"custom_list_id": "{{ custom_list.id }}"}' | ||
| hx-target="#bulk-lists-modal-content" | ||
| hx-swap="innerHTML">Add</button> | ||
| </li> | ||
| {% empty %} | ||
| <li class="text-gray-200">You haven't created any lists yet.</li> | ||
| {% endfor %} | ||
| </ul> | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <div class="bg-[#2a2f35] p-6 rounded-lg text-center" | ||
| x-init="setTimeout(() => { bulkListsOpen = false; Alpine.store('bulk').clear() }, 1500)"> | ||
| <div class="flex flex-col items-center gap-3 py-4"> | ||
| {% include "app/icons/states/completed.svg" with classes="w-10 h-10 text-emerald-400" %} | ||
| <p class="text-white font-medium">Added to list!</p> | ||
| </div> | ||
| </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.
The current implementation for fetching the
CustomListand checking for edit permissions can be improved for security and efficiency.request.POST["custom_list_id"]directly will raise aMultiValueDictKeyErrorif the key is missing. It's safer to userequest.POST.get()and handle the case where it's not provided.get_object_or_404query. This also makes the view consistent withlist_item_toggle.Here's a suggested refactoring that addresses both points and simplifies the code by removing the
if/elseblock for permissions.