mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-09-13 05:12:03 +02:00
feat(ui:transaction): deleting transactions is smoother and no longer requires a full list refresh
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -63,7 +64,8 @@ def bulk_unpay_transactions(request):
|
|||||||
def bulk_delete_transactions(request):
|
def bulk_delete_transactions(request):
|
||||||
selected_transactions = request.GET.getlist("transactions", [])
|
selected_transactions = request.GET.getlist("transactions", [])
|
||||||
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
transactions = Transaction.objects.filter(id__in=selected_transactions)
|
||||||
count = transactions.count()
|
deleted_ids = [str(pk) for pk in transactions.values_list("id", flat=True)]
|
||||||
|
count = len(deleted_ids)
|
||||||
transactions.delete()
|
transactions.delete()
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
@@ -76,9 +78,20 @@ def bulk_delete_transactions(request):
|
|||||||
% {"count": count},
|
% {"count": count},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The list isn't reloaded, the client removes the rows it already has and
|
||||||
|
# drops any divider left empty.
|
||||||
|
# See templates/includes/scripts/hyperscript/transactions.html
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
headers={"HX-Trigger": "updated"},
|
headers={
|
||||||
|
"HX-Trigger": json.dumps(
|
||||||
|
{
|
||||||
|
"transactions_deleted": {"ids": deleted_ids},
|
||||||
|
"selective_update": None,
|
||||||
|
"toasts": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from apps.common.decorators.demo import disabled_on_demo
|
from apps.common.decorators.demo import disabled_on_demo
|
||||||
@@ -478,9 +479,20 @@ def transaction_delete(request, transaction_id, **kwargs):
|
|||||||
|
|
||||||
messages.success(request, _("Transaction deleted successfully"))
|
messages.success(request, _("Transaction deleted successfully"))
|
||||||
|
|
||||||
|
# The list isn't reloaded, the client removes the row it already has and
|
||||||
|
# drops any divider left empty.
|
||||||
|
# See templates/includes/scripts/hyperscript/transactions.html
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
status=204,
|
status=204,
|
||||||
headers={"HX-Trigger": "updated"},
|
headers={
|
||||||
|
"HX-Trigger": json.dumps(
|
||||||
|
{
|
||||||
|
"transactions_deleted": {"ids": [str(transaction_id)]},
|
||||||
|
"selective_update": None,
|
||||||
|
"toasts": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% load markdown %}
|
{% load markdown %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div
|
<div
|
||||||
|
{% if transaction.id and not dummy %}data-transaction-id="{{ transaction.id }}"{% endif %}
|
||||||
class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} group/transaction">
|
class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} group/transaction">
|
||||||
<div class="flex my-1">
|
<div class="flex my-1">
|
||||||
{% if not disable_selection or not dummy %}
|
{% if not disable_selection or not dummy %}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
{% include 'includes/scripts/hyperscript/init_tom_select.html' %}
|
{% include 'includes/scripts/hyperscript/init_tom_select.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/init_date_picker.html' %}
|
{% include 'includes/scripts/hyperscript/init_date_picker.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
|
{% include 'includes/scripts/hyperscript/htmx_error_handler.html' %}
|
||||||
|
{% include 'includes/scripts/hyperscript/transactions.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/sounds.html' %}
|
{% include 'includes/scripts/hyperscript/sounds.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/swal.html' %}
|
{% include 'includes/scripts/hyperscript/swal.html' %}
|
||||||
{% include 'includes/scripts/hyperscript/autosize.html' %}
|
{% include 'includes/scripts/hyperscript/autosize.html' %}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<script type="text/hyperscript">
|
||||||
|
-- Removes deleted transactions from the lists without reloading them.
|
||||||
|
--
|
||||||
|
-- The delete endpoints answer with a `transactions_deleted` HX-Trigger
|
||||||
|
-- carrying the affected ids instead of firing `updated`, which every
|
||||||
|
-- transaction list listens to and would answer with a full re-render.
|
||||||
|
--
|
||||||
|
-- transaction_remover is installed once on the body wrapper so a page with
|
||||||
|
-- hundreds of rows still only holds a single listener. Once the rows are
|
||||||
|
-- gone it announces transactions_removed, and every divider decides for
|
||||||
|
-- itself whether it still has anything to show.
|
||||||
|
|
||||||
|
behavior transaction_remover
|
||||||
|
on transactions_deleted from window
|
||||||
|
set ids to event.detail.ids
|
||||||
|
if no ids then exit end
|
||||||
|
|
||||||
|
set rows to []
|
||||||
|
set lists to []
|
||||||
|
repeat for row in <.transaction[data-transaction-id]/>
|
||||||
|
if ids.includes(the @data-transaction-id of row)
|
||||||
|
append row to rows
|
||||||
|
set list to the closest <#transactions-list/> to row
|
||||||
|
if list and not lists.includes(list) then append list to lists end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if rows.length is 0 then exit end
|
||||||
|
|
||||||
|
-- freeze the current height so it has somewhere to animate from
|
||||||
|
repeat for row in rows
|
||||||
|
set *height of row to `${row.offsetHeight}px`
|
||||||
|
set *overflow of row to 'hidden'
|
||||||
|
end
|
||||||
|
|
||||||
|
repeat for row in rows
|
||||||
|
get row.offsetHeight -- force a reflow so the height above is the start
|
||||||
|
add .transaction-removing to row
|
||||||
|
set *height of row to '0px'
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `wait for transitionend` can only watch `me`, which here is the wrapper,
|
||||||
|
-- so the rows get a plain wait. Keep it just above the .transaction-removing
|
||||||
|
-- duration in _animations.scss.
|
||||||
|
wait 300ms
|
||||||
|
|
||||||
|
repeat for row in rows
|
||||||
|
remove row
|
||||||
|
end
|
||||||
|
|
||||||
|
trigger transactions_removed on window
|
||||||
|
|
||||||
|
repeat for list in lists
|
||||||
|
send change to list -- the selection action bar recounts what's left
|
||||||
|
set remaining to <.transaction/> in list
|
||||||
|
if remaining.length is 0
|
||||||
|
-- nothing left to show, let the server render the empty state
|
||||||
|
trigger updated on window
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
behavior transaction_divider
|
||||||
|
on transactions_removed from window
|
||||||
|
set remaining to <.transaction/> in me
|
||||||
|
if remaining.length is not 0 then exit end
|
||||||
|
|
||||||
|
set *height of me to `${my offsetHeight}px`
|
||||||
|
set *overflow of me to 'hidden'
|
||||||
|
get my offsetHeight
|
||||||
|
add .transaction-removing to me
|
||||||
|
set *height of me to '0px'
|
||||||
|
wait for transitionend or 1s
|
||||||
|
remove me
|
||||||
|
end
|
||||||
|
</script>
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
<body class="font-mono">
|
<body class="font-mono">
|
||||||
<div _="install htmx_error_handler
|
<div _="install htmx_error_handler
|
||||||
|
install transaction_remover
|
||||||
{% block body_hyperscript %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
{% block body_hyperscript %}{% endblock %}" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
{% include 'includes/mobile_navbar.html' %}
|
{% include 'includes/mobile_navbar.html' %}
|
||||||
{% include 'includes/sidebar.html' %}
|
{% include 'includes/sidebar.html' %}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<div id="transactions-list">
|
<div id="transactions-list">
|
||||||
{% if late_transactions %}
|
{% if late_transactions %}
|
||||||
<div id="late-transactions" class="transactions-divider"
|
<div id="late-transactions" class="transactions-divider"
|
||||||
|
_="install transaction_divider"
|
||||||
x-data="{ open: sessionStorage.getItem('late-transactions') !== 'false' }"
|
x-data="{ open: sessionStorage.getItem('late-transactions') !== 'false' }"
|
||||||
x-init="if (sessionStorage.getItem('late-transactions') === null) sessionStorage.setItem('late-transactions', 'true')">
|
x-init="if (sessionStorage.getItem('late-transactions') === null) sessionStorage.setItem('late-transactions', 'true')">
|
||||||
<div class="mt-3 mb-1 w-full border-b border-b-error/50 transactions-divider-title cursor-pointer">
|
<div class="mt-3 mb-1 w-full border-b border-b-error/50 transactions-divider-title cursor-pointer">
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
|
|
||||||
{% for x in transactions_by_date %}
|
{% for x in transactions_by_date %}
|
||||||
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
||||||
|
_="install transaction_divider"
|
||||||
x-data="{ open: sessionStorage.getItem('{{ x.grouper|slugify }}') !== 'false' }"
|
x-data="{ open: sessionStorage.getItem('{{ x.grouper|slugify }}') !== 'false' }"
|
||||||
x-init="if (sessionStorage.getItem('{{ x.grouper|slugify }}') === null) sessionStorage.setItem('{{ x.grouper|slugify }}', 'true')">
|
x-init="if (sessionStorage.getItem('{{ x.grouper|slugify }}') === null) sessionStorage.setItem('{{ x.grouper|slugify }}', 'true')">
|
||||||
<div class="mt-3 mb-1 w-full border-b border-b-base-content/30 transactions-divider-title cursor-pointer">
|
<div class="mt-3 mb-1 w-full border-b border-b-base-content/30 transactions-divider-title cursor-pointer">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<div id="transactions-list" class="show-loading">
|
<div id="transactions-list" class="show-loading">
|
||||||
{% if late_transactions %}
|
{% if late_transactions %}
|
||||||
<div id="late-transactions" class="transactions-divider"
|
<div id="late-transactions" class="transactions-divider"
|
||||||
|
_="install transaction_divider"
|
||||||
x-data="{ open: sessionStorage.getItem('late-transactions') !== 'false' }"
|
x-data="{ open: sessionStorage.getItem('late-transactions') !== 'false' }"
|
||||||
x-init="if (sessionStorage.getItem('late-transactions') === null) sessionStorage.setItem('late-transactions', 'true')">
|
x-init="if (sessionStorage.getItem('late-transactions') === null) sessionStorage.setItem('late-transactions', 'true')">
|
||||||
<div class="mt-3 mb-1 w-full border-b border-b-error/50 transactions-divider-title cursor-pointer">
|
<div class="mt-3 mb-1 w-full border-b border-b-error/50 transactions-divider-title cursor-pointer">
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
|
|
||||||
{% for x in transactions_by_date %}
|
{% for x in transactions_by_date %}
|
||||||
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
<div id="{{ x.grouper|slugify }}" class="transactions-divider"
|
||||||
|
_="install transaction_divider"
|
||||||
x-data="{ open: sessionStorage.getItem('{{ x.grouper|slugify }}') !== 'false' }"
|
x-data="{ open: sessionStorage.getItem('{{ x.grouper|slugify }}') !== 'false' }"
|
||||||
x-init="if (sessionStorage.getItem('{{ x.grouper|slugify }}') === null) sessionStorage.setItem('{{ x.grouper|slugify }}', 'true')">
|
x-init="if (sessionStorage.getItem('{{ x.grouper|slugify }}') === null) sessionStorage.setItem('{{ x.grouper|slugify }}', 'true')">
|
||||||
<div class="mt-3 mb-1 w-full border-b border-b-base-content/30 transactions-divider-title cursor-pointer">
|
<div class="mt-3 mb-1 w-full border-b border-b-base-content/30 transactions-divider-title cursor-pointer">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ volumes:
|
|||||||
wygiwyh_dev_postgres_data: {}
|
wygiwyh_dev_postgres_data: {}
|
||||||
wygiwyh_temp:
|
wygiwyh_temp:
|
||||||
wygiwyh_attachments:
|
wygiwyh_attachments:
|
||||||
|
wygiwyh_node_modules:
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -35,7 +36,7 @@ services:
|
|||||||
- ./frontend/:/usr/src/frontend
|
- ./frontend/:/usr/src/frontend
|
||||||
- ./app/:/usr/src/app/
|
- ./app/:/usr/src/app/
|
||||||
# http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html
|
# http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html
|
||||||
- /usr/src/frontend/node_modules
|
- wygiwyh_node_modules:/usr/src/frontend/node_modules
|
||||||
ports:
|
ports:
|
||||||
- '${WEBPACK_OUTBOUND_PORT}:5173'
|
- '${WEBPACK_OUTBOUND_PORT}:5173'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -300,4 +300,21 @@
|
|||||||
100% {
|
100% {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transaction rows and dividers on their way out.
|
||||||
|
// See templates/includes/scripts/hyperscript/transactions.html, which waits on
|
||||||
|
// transitionend, so every property here shares one duration and reduced motion
|
||||||
|
// shortens it rather than removing it.
|
||||||
|
.transaction-removing {
|
||||||
|
transition-property: height, opacity, transform;
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transition-timing-function: cubic-bezier(0.55, 0.085, 0.68, 0.53);
|
||||||
|
transform: translateX(-1.5rem);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
transition-duration: 0.01s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user