feat: add admin views, a booking horizon and confirmation modals
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
8caf618950
commit
1a3640b15c
27 files changed
+1269
-62
No files matched your search
@@ -0,0 +1,52 @@
|
||||
{#
|
||||
What an administrator sees when clicking an occupied shape on the floor plan.
|
||||
|
||||
Not the edit modal: a shape can hold several bookings on one day once part-day spans exist,
|
||||
so "the booking" has no single referent -- and an administrator releases rather than
|
||||
reschedules. Each row posts to the ordinary app_booking_release route, whose ownership check
|
||||
admits an admin, so there is still one code path that removes a booking.
|
||||
#}
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
{% block title %}{{ resource.label }} verwalten{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p class="pb-4 text-sm text-gray-500">
|
||||
{{ resource.type.label }} · {{ date|format_datetime(pattern: 'EEEE, d. MMMM y') }}
|
||||
{%- if resource.group %} · {{ resource.group.label }}{% endif %}
|
||||
</p>
|
||||
|
||||
{% if bookings is empty %}
|
||||
<p class="text-sm text-gray-500">In diesem Zeitraum liegt hier keine Buchung.</p>
|
||||
{% else %}
|
||||
<ul class="divide-y divide-gray-200 border-y border-gray-200">
|
||||
{% for booking in bookings %}
|
||||
<li class="flex items-center justify-between gap-4 py-3">
|
||||
<div>
|
||||
<div class="font-semibold">{{ booking.user.name }}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ booking.timeRange }}
|
||||
{% if booking.partOfSeries %}
|
||||
<span class="badge bg-gray-100 text-gray-600 ml-1">Serie</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{# Same as the edit modal: opens the shared confirmation modal over this one.
|
||||
Confirming repaints the board out of band and removes both. #}
|
||||
<button type="button" class="button button--small button--warning"
|
||||
hx-get="{{ path('app_booking_release', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Freigeben
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex justify-end pt-6">
|
||||
<button type="button" class="button button--neutral" {{ stimulus_action('modal', 'close') }}>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,104 @@
|
||||
{#
|
||||
Every booking, not just yours. The point of the screen is the "Freigeben" button on somebody
|
||||
else's row: when a colleague calls in sick, their desk has to go back into the pool.
|
||||
|
||||
Filtering is a plain GET form -- the house rule hands navigation to the browser. The two
|
||||
destructive actions open the shared confirmation modal via htmx and then navigate back here,
|
||||
which is what ?return=admin says; without it a confirmed release swaps board fragments this
|
||||
page does not have.
|
||||
#}
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block title %}Verwaltung · ephub{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<a class="text-sm text-gray-500 hover:text-gray-900" href="{{ path('app_dashboard') }}">← Übersicht</a>
|
||||
|
||||
<h1 class="pt-2">Verwaltung</h1>
|
||||
<p class="subline">Buchungen aller Mitarbeitenden.</p>
|
||||
|
||||
{{ form_start(form, { attr: { class: 'mt-6 rounded-md border border-gray-200 p-4' } }) }}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{{ form_row(form.from) }}
|
||||
{{ form_row(form.to) }}
|
||||
{{ form_row(form.type) }}
|
||||
{{ form_row(form.user) }}
|
||||
</div>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<a class="button button--neutral" href="{{ path('app_admin_booking') }}">Zurücksetzen</a>
|
||||
<button type="submit" class="button button--primary">Filtern</button>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% if series is not empty %}
|
||||
<h2 class="pt-8">Serien</h2>
|
||||
<ul class="divide-y divide-gray-200 border-y border-gray-200">
|
||||
{% for entry in series %}
|
||||
<li class="flex items-center justify-between gap-4 py-3">
|
||||
<div>
|
||||
<div class="font-semibold">
|
||||
{{ entry.user.name }}
|
||||
<span class="font-normal text-gray-500">· {{ entry.resource.label }}</span>
|
||||
{%- if entry.resource.group %} <span class="font-normal text-gray-500">· {{ entry.resource.group.label }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ entry.describe }} · bis {{ entry.endDate|format_datetime(pattern: 'd. MMMM y') }}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="button button--small button--neutral"
|
||||
hx-get="{{ path('app_series_end', { id: entry.id, return: 'admin' }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Serie beenden
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="pt-8">Termine</h2>
|
||||
<p class="subline">
|
||||
{{ filter.fromDate|format_datetime(pattern: 'd. MMMM y') }} bis
|
||||
{{ filter.toDate|format_datetime(pattern: 'd. MMMM y') }}
|
||||
</p>
|
||||
|
||||
{% if bookings is empty %}
|
||||
<p class="text-sm text-gray-500">Keine Buchungen in diesem Zeitraum.</p>
|
||||
{% else %}
|
||||
<ul class="divide-y divide-gray-200 border-y border-gray-200">
|
||||
{% for booking in bookings %}
|
||||
<li class="flex items-center justify-between gap-4 py-3">
|
||||
<div>
|
||||
<div class="font-semibold">
|
||||
{{ booking.user.name }}
|
||||
<span class="font-normal text-gray-500">
|
||||
· {{ booking.date|format_datetime(pattern: 'EEE, d. MMM y') }}
|
||||
· {{ booking.timeRange }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ booking.resource.type.label }} · {{ booking.resource.label }}
|
||||
{%- if booking.resource.group %} · {{ booking.resource.group.label }}{% endif %}
|
||||
{% if booking.partOfSeries %}
|
||||
<span class="badge bg-gray-100 text-gray-600 ml-1">Serie</span>
|
||||
{% endif %}
|
||||
{% if booking.overridden %}
|
||||
<span class="badge bg-amber-100 text-amber-800 ml-1">abweichend</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a class="text-sm underline shrink-0"
|
||||
href="{{ path('app_board', { type: booking.resource.type.value, date: booking.date|date('Y-m-d') }) }}">Im Plan</a>
|
||||
<button type="button" class="button button--small button--neutral"
|
||||
hx-get="{{ path('app_booking_release', { id: booking.id, return: 'admin' }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Freigeben
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -37,6 +37,11 @@
|
||||
{% set classes = classes ~ ' border-primary bg-primary text-white font-bold' %}
|
||||
{% elseif not day.in_month %}
|
||||
{% set classes = classes ~ ' border-transparent text-gray-300' %}
|
||||
{% elseif day.is_past %}
|
||||
{# Dashed rather than the weekend's grey fill: the two states must not be
|
||||
confusable, and the day stays clickable -- looking back at a past plan is
|
||||
legitimate, only booking it is not. #}
|
||||
{% set classes = classes ~ ' border-dashed border-gray-200 text-gray-400' %}
|
||||
{% elseif day.is_weekend %}
|
||||
{% set classes = classes ~ ' border-transparent bg-gray-50 text-gray-400' %}
|
||||
{% else %}
|
||||
@@ -49,15 +54,16 @@
|
||||
<button type="button"
|
||||
class="{{ classes }}"
|
||||
aria-pressed="{{ day.is_selected ? 'true' : 'false' }}"
|
||||
{%- if day.is_past %} aria-disabled="true" title="Vergangener Tag – keine Buchung mehr möglich"{% endif %}
|
||||
hx-get="{{ path('app_board_day', { type: type.value, date: day.date|date('Y-m-d') }|merge(time_params)) }}"
|
||||
hx-swap="none">
|
||||
<span>{{ day.date|date('j') }}</span>
|
||||
{# Team view: the occupancy count is the whole point of showing the month. #}
|
||||
<span class="text-[10px] {{ day.is_selected ? 'text-white/80' : 'text-gray-400' }}">
|
||||
<span class="text-[10px] {{ day.is_selected ? 'text-white/80' : (day.is_past ? 'text-gray-300' : 'text-gray-400') }}">
|
||||
{{ day.count > 0 ? day.count : '' }}
|
||||
</span>
|
||||
{% if day.mine is not empty %}
|
||||
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full {{ day.is_selected ? 'bg-white' : 'bg-primary' }}"></span>
|
||||
<span class="absolute top-1 right-1 w-1.5 h-1.5 rounded-full {{ day.is_selected ? 'bg-white' : (day.is_past ? 'bg-gray-300' : 'bg-primary') }}"></span>
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
@@ -68,6 +74,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary"></span> eigene Buchung
|
||||
</div>
|
||||
<div>gestrichelt = vergangen, nicht mehr buchbar</div>
|
||||
<div>Zahl = belegte Plätze{{ selected_time.allDay ? ' an dem Tag' : ' zwischen ' ~ selected_time.start ~ ' und ' ~ selected_time.end }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,3 +32,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{#
|
||||
Removes whatever modal is open, out of band. Only BookingController::boardResponse() renders
|
||||
it, and only for a confirmation that was opened over another modal -- the confirmation swaps
|
||||
itself away by targeting its own element, which leaves the one underneath behind.
|
||||
|
||||
Never rendered as part of the page: nothing in the layout calls this block.
|
||||
#}
|
||||
{% block modal_dismiss %}
|
||||
<div id="htmx-modal" hx-swap-oob="delete"></div>
|
||||
{% endblock %}
|
||||
@@ -53,6 +53,14 @@
|
||||
{% endif %}
|
||||
|
||||
{% set interactive = not is_past and spot.actionable %}
|
||||
{# An administrator may free somebody else's desk -- a colleague calls in sick and the spot
|
||||
should go back into the pool. Colours are untouched: MINE still means mine, and only
|
||||
clickability widens. Past days stay inert for admins too; there is no past-write path.
|
||||
|
||||
Only a shape that is not already actionable: a partly booked one still opens the create
|
||||
modal, because taking the free window is the more useful act there and an admin can reach
|
||||
that booking from /admin/bookings anyway. #}
|
||||
{% set manageable = not is_past and not interactive and is_granted('ROLE_ADMIN') and spot.bookings is not empty %}
|
||||
|
||||
{# One line per booking, then the window still free if there is one -- that last line is the
|
||||
reason a partly booked shape is worth clicking, so it earns its place. #}
|
||||
@@ -77,8 +85,8 @@
|
||||
{% set lines = lines|slice(0, MAX_LINES - 1)|merge(['+' ~ (lines|length - MAX_LINES + 1) ~ ' weitere']) %}
|
||||
{% endif %}
|
||||
|
||||
<g class="{{ interactive ? 'cursor-pointer' : 'cursor-default' }}"
|
||||
{%- if interactive %} role="button"{% endif %}
|
||||
<g class="{{ interactive or manageable ? 'cursor-pointer' : 'cursor-default' }}"
|
||||
{%- if interactive or manageable %} role="button"{% endif %}
|
||||
aria-label="{{ spot.resource.label }}{% if lines is empty %} — frei{% else %} — {{ lines|join(', ') }}{% endif %}"
|
||||
{%- if interactive and state == 'MINE' %}
|
||||
hx-get="{{ path('app_booking_edit', { id: spot.own.id }|merge(time_params)) }}"
|
||||
@@ -90,6 +98,12 @@
|
||||
hx-get="{{ path('app_booking_new', { id: spot.resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
{%- elseif manageable %}
|
||||
{# Not app_booking_edit: a shape can hold several bookings on one day, and an admin
|
||||
releases rather than reschedules. The modal lists them and frees one. #}
|
||||
hx-get="{{ path('app_admin_spot', { id: spot.resource.id, date: selected_date|date('Y-m-d') }|merge(time_params)) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
{%- endif %}>
|
||||
<rect x="{{ geometry.x }}" y="{{ geometry.y }}" width="{{ width }}" height="{{ height }}" rx="6"
|
||||
class="{{ fill }} {{ stroke }}" stroke-width="2"></rect>
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
differing from it.
|
||||
|
||||
Releasing lives here too rather than on the floor plan, so the SVG needs no confirm dialogs
|
||||
or inline CSRF tokens -- clicking your own desk always just opens this.
|
||||
or inline CSRF tokens -- clicking your own desk always just opens this. The release button
|
||||
opens the shared confirmation modal over this one.
|
||||
#}
|
||||
{% extends 'htmx_modal.html.twig' %}
|
||||
|
||||
@@ -32,14 +33,13 @@
|
||||
{% include 'booking/_fields.html.twig' %}
|
||||
|
||||
<div class="flex flex-wrap justify-between gap-2 pt-6">
|
||||
{# type="button" with its own hx-post: htmx issues the release request directly, so
|
||||
this never submits the surrounding edit form and no nested <form> is needed. #}
|
||||
{# type="button" with its own hx-get: htmx opens the confirmation modal over this
|
||||
one, so nothing submits the surrounding edit form and no nested <form> is needed.
|
||||
Confirming removes both modals -- see BookingController::boardResponse(). #}
|
||||
<button type="button" class="button button--small button--warning"
|
||||
hx-post="{{ path('app_booking_release', { id: booking.id }) }}"
|
||||
hx-vals='{{ { _token: csrf_token('release-' ~ booking.id) }|json_encode }}'
|
||||
hx-confirm="{{ resource.label }} am {{ booking.date|date('d.m.Y') }} freigeben?"
|
||||
hx-target="#htmx-modal"
|
||||
hx-swap="outerHTML">
|
||||
hx-get="{{ path('app_booking_release', { id: booking.id }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Freigeben
|
||||
</button>
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block title %}Serie beenden?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
{{ series.resource.label }} · {{ series.describe }}
|
||||
{% if series.user.id != app.user.id %}
|
||||
— Serie von <em>{{ series.user.name }}</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="pt-3 text-sm text-gray-500">
|
||||
Alle Termine ab heute werden entfernt. Vergangene Termine bleiben als Historie stehen.
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block button_confirm %}Serie beenden{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends 'htmx_confirmation_modal.html.twig' %}
|
||||
|
||||
{% block title %}Buchung freigeben?{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
{{ booking.resource.type.label }} <em>{{ booking.resource.label }}</em>
|
||||
{%- if booking.resource.group %} ({{ booking.resource.group.label }}){% endif %}
|
||||
am {{ booking.date|format_datetime(pattern: 'EEEE, d. MMMM y') }}, {{ booking.timeRange }}
|
||||
{% if not booking.belongsTo(app.user) %}
|
||||
— gebucht von <em>{{ booking.user.name }}</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if booking.partOfSeries %}
|
||||
<p class="pt-3 text-sm text-gray-500">
|
||||
Teil einer Serie: {{ booking.series.describe }}. Es wird nur dieser Tag freigegeben.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block button_confirm %}Freigeben{% endblock %}
|
||||
@@ -22,12 +22,14 @@
|
||||
{{ entry.describe }} · bis {{ entry.endDate|format_datetime(pattern: 'd. MMMM y') }}
|
||||
</div>
|
||||
</div>
|
||||
{# Ends the series from today on; past occurrences are history and stay put. #}
|
||||
<form method="post" action="{{ path('app_series_end', { id: entry.id }) }}"
|
||||
onsubmit="return confirm('Serie ab heute beenden? Alle künftigen Termine werden entfernt.')">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('end-series-' ~ entry.id) }}">
|
||||
<button type="submit" class="button button--small button--neutral">Serie beenden</button>
|
||||
</form>
|
||||
{# Opens the confirmation modal; the CSRF token and the actual POST live in
|
||||
there, which is why this row carries neither a form nor a token. #}
|
||||
<button type="button" class="button button--small button--neutral"
|
||||
hx-get="{{ path('app_series_end', { id: entry.id, return: 'bookings' }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Serie beenden
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -62,12 +64,14 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<a class="text-sm underline"
|
||||
href="{{ path('app_board', { type: booking.resource.type.value, date: booking.date|date('Y-m-d') }) }}">Im Plan</a>
|
||||
{# Ordinary form post: this page has no board fragments to swap, so it
|
||||
reloads afterwards rather than pretending to be htmx. #}
|
||||
<form method="post" action="{{ path('app_booking_release', { id: booking.id }) }}">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('release-' ~ booking.id) }}">
|
||||
<button type="submit" class="button button--small button--neutral">Freigeben</button>
|
||||
</form>
|
||||
{# ?return=bookings: this page has no board fragments to swap, so the
|
||||
confirmed release navigates back here instead. #}
|
||||
<button type="button" class="button button--small button--neutral"
|
||||
hx-get="{{ path('app_booking_release', { id: booking.id, return: 'bookings' }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
Freigeben
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -47,6 +47,23 @@
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 pb-4">Alle Termine und Serien verwalten.</p>
|
||||
</a>
|
||||
|
||||
{#
|
||||
The header is deliberately not navigation, so the dashboard is the only place an
|
||||
admin screen can be reached from.
|
||||
#}
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
<a href="{{ path('app_admin_booking') }}"
|
||||
class="group flex flex-col rounded-md border border-gray-200 p-6 hover:border-primary hover:bg-gray-50">
|
||||
<div class="flex items-center gap-3 pb-2">
|
||||
{{ ux_icon('lucide:shield-check', { class: 'h-6 w-6 shrink-0 text-primary' }) }}
|
||||
<span class="text-lg font-bold">Verwaltung</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 pb-4">
|
||||
Buchungen aller Mitarbeitenden einsehen und ggf. freigeben.
|
||||
</p>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2 class="pt-10">Als Nächstes</h2>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{#
|
||||
The shared "are you sure?" modal, ported from myep/myep-team's htmx_confirmation_modal.html.twig.
|
||||
|
||||
It replaces both native confirmation mechanisms this app used before: htmx's `hx-confirm`
|
||||
(a window.confirm, unstyleable and invisible to the page) and `onsubmit="return confirm(...)"`
|
||||
on the list forms. Both are gone; neither may come back.
|
||||
|
||||
The flow is the sibling one: a route serves GET (render this) and POST (do the thing), the
|
||||
opener is a plain `hx-get` button appending to <body>, and the confirm button posts back to
|
||||
the *same* URI -- so the query string that brought the user here, `?return=` included, is
|
||||
carried over for free.
|
||||
|
||||
The CSRF token lives here rather than on the opener, which is why the lists no longer carry
|
||||
hidden token fields at all.
|
||||
|
||||
Blocks: title, content, button_attribute (replace the hx-* defaults), button_confirm,
|
||||
button_cancel. Callers must pass `csrf_token_id`.
|
||||
#}
|
||||
<div id="htmx-confirmation-modal"
|
||||
class="fixed inset-0 w-full h-full z-50"
|
||||
{{ stimulus_controller('modal') }}
|
||||
{{ stimulus_action('modal', 'close', 'modal-close@window') }}>
|
||||
<div class="absolute inset-0 w-full h-full bg-black/70" {{ stimulus_action('modal', 'close', 'click') }}></div>
|
||||
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 px-4 w-full max-w-xl">
|
||||
<div class="relative bg-white py-8 rounded-md">
|
||||
<div class="flex justify-between pb-4 px-8">
|
||||
<div class="flex items-center gap-2">
|
||||
{{ ux_icon('lucide:triangle-alert', { class: 'w-7 h-7 shrink-0 text-amber-500' }) }}
|
||||
<div class="text-2xl font-bold">
|
||||
{% block title %}Bist du sicher?{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-10 h-10"
|
||||
{{ stimulus_action('modal', 'close') }}>
|
||||
{{ ux_icon('lucide:x', { class: 'w-8 h-8' }) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-8">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<div class="flex justify-end gap-2 pt-6">
|
||||
<button type="button" class="button button--neutral" {{ stimulus_action('modal', 'close') }}>
|
||||
{% block button_cancel %}Abbrechen{% endblock %}
|
||||
</button>
|
||||
{#
|
||||
Targets itself with outerHTML, so a response consisting only of
|
||||
out-of-band blocks leaves nothing to swap in and this modal disappears --
|
||||
the same mechanism the booking modal uses. A response that instead sets
|
||||
HX-Redirect navigates away and the swap never happens.
|
||||
#}
|
||||
<button type="button"
|
||||
class="button button--warning"
|
||||
hx-target="#htmx-confirmation-modal"
|
||||
hx-swap="outerHTML"
|
||||
{% block button_attribute %}
|
||||
hx-post="{{ app.request.uri }}"
|
||||
hx-vals="{{ { _token: csrf_token(csrf_token_id) }|json_encode|e('html_attr') }}"
|
||||
{% endblock %}>
|
||||
{% block button_confirm %}Ja{% endblock %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user