mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 10:04:18 +00:00
Put the Delete forever buttons behind a Disposal setting, off by default (#71)
Settings → Mail → Disposal has one per-user switch, off until somebody turns it on. It decides whether Delete forever exists at all: the flame on a row in Spam and the bin, and a new Delete forever above a selection there. Both purge routes answer 403 while the switch is off. A selection extended with Select all N empties the whole of Spam or the bin in a background job. Any other view is refused, and the worker checks the switch and each conversation's placement again. The bin no longer offers Delete above a selection: it trashed mail that was already in the bin, took the rows off the page and they were back on the next load. The choice lives in the user's settings bag and a config backup carries it. No migration. Continues #56. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99d2da3da2
commit
f19818fa50
30 files changed
+1873
-13
No files matched your search
@@ -133,6 +133,8 @@ closes the menu: ui--dropdown closes on a click OUTSIDE it, and a row is inside.
|
||||
selectAllAction: 'toolbar.view_select.offer_action'|trans,
|
||||
allSelected: 'toolbar.view_select.selected'|trans,
|
||||
clear: 'toolbar.view_select.clear'|trans,
|
||||
purgeConfirmOne: 'toolbar.purge_confirm.one'|trans,
|
||||
purgeConfirmMany: 'toolbar.purge_confirm.many'|trans,
|
||||
}|json_encode }}"
|
||||
>
|
||||
|
||||
@@ -280,7 +282,25 @@ closes the menu: ui--dropdown closes on a click OUTSIDE it, and a row is inside.
|
||||
aria-hidden="true"
|
||||
>
|
||||
{{ m.tbtn('click->mail--list-toolbar#archiveSelected', 'fa-solid fa-box-archive', 'toolbar.actions.archive'|trans) }}
|
||||
{{ m.tbtn('click->mail--list-toolbar#deleteSelected', 'fa-solid fa-trash', 'toolbar.actions.delete'|trans, 'hover:text-danger') }}
|
||||
{# Delete, or Delete forever where the selection is already thrown away.
|
||||
|
||||
Delete forever is the user's choice (Settings → Mail → Disposal), off
|
||||
until they make it, and is the one button here that cannot be undone.
|
||||
With it on, Spam and the bin offer it in place of Delete, as the rows
|
||||
there do: in Spam the trash would only be a second place to delete the
|
||||
same mail from, and in the bin it trashes mail that already carries
|
||||
Trash, which changes nothing on the server while the answer still
|
||||
takes the rows out of the list, so mail looked deleted and was back on
|
||||
the next load.
|
||||
|
||||
With it off, Spam keeps Delete, and the bin has no Delete at all, for
|
||||
the second reason: the only way out of it is Move to inbox. What asks
|
||||
first before a delete forever is mail--list-toolbar#purgeSelected. #}
|
||||
{% if list_scope|default('') in ['spam', 'trash'] and app.user and app.user.deleteNowButtons %}
|
||||
{{ m.tbtn('click->mail--list-toolbar#purgeSelected', 'fa-solid fa-fire', 'toolbar.actions.purge'|trans, 'hover:text-danger') }}
|
||||
{% elseif 'trash' != list_scope|default('') %}
|
||||
{{ m.tbtn('click->mail--list-toolbar#deleteSelected', 'fa-solid fa-trash', 'toolbar.actions.delete'|trans, 'hover:text-danger') }}
|
||||
{% endif %}
|
||||
{# Beside archive and delete because it is the third way a selection
|
||||
LEAVES this list, and those three are what a selection is nearly
|
||||
always made for. #}
|
||||
|
||||
@@ -751,8 +751,14 @@ be a worse regression than the one this feature fixes.
|
||||
data-turbo-confirm: Turbo only consults that attribute
|
||||
for links and form submissions, and this posts with
|
||||
fetch(). The controller asks the application's dialog
|
||||
itself — see mail--message-row#purge. #}
|
||||
{% if placement in ['trash', 'spam'] %}
|
||||
itself — see mail--message-row#purge.
|
||||
|
||||
And only for somebody who has switched it on, in
|
||||
Settings → Mail → Disposal: nothing in the list that
|
||||
cannot be taken back should be one click away until
|
||||
they have asked for it. The route refuses without the
|
||||
switch as well, so this is not the only guard. #}
|
||||
{% if placement in ['trash', 'spam'] and app.user and app.user.deleteNowButtons %}
|
||||
<button type="button" data-action="click->mail--message-row#purge"
|
||||
data-mail--message-row-purge-url-param="{{ path('app_status_purge', {'type': type, 'id': rowId}) }}"
|
||||
data-mail--message-row-purge-confirm-param="{{ 'discarded.actions.purge_confirm'|trans }}"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{#
|
||||
settings/_disposal.html.twig
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
Whether the buttons that delete mail for good are offered: the flame on a row
|
||||
in Spam and the bin, and the same button above a selection there.
|
||||
|
||||
A plain form that submits on change and redirects, like the cards beside it:
|
||||
the switch decides what the NEXT list draws, so there is nothing on this page
|
||||
worth streaming back.
|
||||
|
||||
Off to begin with, and the card says why: it is the one action in plMail that
|
||||
cannot be undone. The hint names both places so nobody has to find the second
|
||||
one by accident.
|
||||
|
||||
Accepts:
|
||||
deleteNow bool
|
||||
#}
|
||||
{% set segText = 'flex h-full items-center justify-center px-3 py-1.5 text-center text-[11px] leading-tight text-ink-muted
|
||||
peer-checked:bg-accent peer-checked:text-accent-ink
|
||||
peer-focus-visible:ring-2 peer-focus-visible:ring-inset peer-focus-visible:ring-accent
|
||||
transition-colors cursor-pointer' %}
|
||||
|
||||
<div class="px-4 py-3">
|
||||
|
||||
<form method="post" action="{{ path('app_settings_disposal_update') }}" data-turbo="false" data-controller="ui--auto-submit">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('settings-disposal') }}">
|
||||
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<span class="min-w-0">
|
||||
<span class="block text-xs font-medium text-ink-muted">{{ 'settings.disposal.delete_now'|trans }}</span>
|
||||
<span class="block text-[11px] text-ink-faint mt-0.5">{{ 'settings.disposal.delete_now_hint'|trans }}</span>
|
||||
</span>
|
||||
|
||||
<div class="grid grid-flow-col auto-cols-fr shrink-0 rounded-pane border border-line overflow-hidden"
|
||||
role="radiogroup" aria-label="{{ 'settings.disposal.delete_now'|trans }}">
|
||||
{% for on in [true, false] %}
|
||||
<label>
|
||||
<input type="radio" name="deleteNow" class="peer sr-only"
|
||||
value="{{ on ? '1' : '0' }}"
|
||||
data-action="change->ui--auto-submit#submit"
|
||||
{{ deleteNow == on ? 'checked' : '' }}>
|
||||
<span class="{{ segText }}">
|
||||
{{ (on ? 'settings.disposal.on' : 'settings.disposal.off')|trans }}
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -154,6 +154,7 @@
|
||||
{ section: 'templates', icon: 'fa-regular fa-file-lines' },
|
||||
{ section: 'labels', icon: 'fa-solid fa-tag' },
|
||||
{ section: 'filters', icon: 'fa-solid fa-filter' },
|
||||
{ section: 'disposal', icon: 'fa-solid fa-trash-can' },
|
||||
{ section: 'insights', icon: 'fa-solid fa-satellite-dish' },
|
||||
]|merge(ai_settings_available() or section == 'ai'
|
||||
? [{ section: 'ai', icon: 'fa-solid fa-wand-magic-sparkles' }]
|
||||
@@ -421,6 +422,17 @@
|
||||
} only %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% elseif section == 'disposal' %}
|
||||
{% embed 'settings/_card.html.twig' with {
|
||||
heading: 'settings.disposal.heading'|trans,
|
||||
subheading: 'settings.disposal.subheading'|trans,
|
||||
} %}
|
||||
{% block body %}
|
||||
{% include 'settings/_disposal.html.twig' with {
|
||||
deleteNow: app.user.deleteNowButtons,
|
||||
} only %}
|
||||
{% endblock %}
|
||||
{% endembed %}
|
||||
{% elseif section == 'shortcuts' %}
|
||||
{% embed 'settings/_card.html.twig' with {
|
||||
heading: 'settings.shortcuts.heading'|trans,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{#
|
||||
The answer to a bulk delete for good: take each row out of the list.
|
||||
|
||||
Not _bulk.stream.html.twig, which is addressed by the conversations themselves.
|
||||
These no longer exist — the purge clears their ids — so the controller captures
|
||||
the ids first and hands those over, exactly as _purge.stream.html.twig does for
|
||||
the one row. The client re-reads the list frame afterwards for the pager and the
|
||||
empty state, as it does for every bulk action.
|
||||
|
||||
Variables:
|
||||
threadIds the ids of the conversations that were deleted
|
||||
#}
|
||||
<!-- plmail-bulk: {{ threadIds|length }} -->
|
||||
|
||||
{% for id in threadIds %}
|
||||
<turbo-stream action="remove" target="thread_{{ id }}">
|
||||
</turbo-stream>
|
||||
{% endfor %}
|
||||
Reference in new issue
Block a user