Merge pull request #1391 from ae-utbm/notifications-magic

improve `$notifications`
This commit is contained in:
thomas girod
2026-05-20 23:03:30 +02:00
committed by GitHub
11 changed files with 101 additions and 62 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<title>{% trans %}Slideshow{% endtrans %}</title> <title>{% trans %}Slideshow{% endtrans %}</title>
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}"> <link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
<link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" /> <link href="{{ static('css/slideshow.scss') }}" rel="stylesheet" type="text/css" />
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script> <script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script> <script type="module" src="{{ static('bundled/com/slideshow-index.ts') }}"></script>
</head> </head>
<body x-data="slideshow([ <body x-data="slideshow([
-12
View File
@@ -1,12 +0,0 @@
import sort from "@alpinejs/sort";
import Alpine from "alpinejs";
import { limitedChoices } from "#core:alpine/limited-choices.ts";
import { alpinePlugin as notificationPlugin } from "#core:utils/notifications.ts";
Alpine.plugin([sort, limitedChoices]);
Alpine.magic("notifications", notificationPlugin);
window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
+21
View File
@@ -0,0 +1,21 @@
import sort from "@alpinejs/sort";
import { Alpine } from "alpinejs";
import { limitedChoices } from "#core:alpine/limited-choices";
import {
type NotificationPlugin,
notificationsPlugin as notifications,
} from "#core:utils/notifications";
declare module "alpinejs" {
interface Magics<T> {
$notifications: NotificationPlugin;
}
}
Alpine.plugin([sort, limitedChoices, notifications]);
// biome-ignore lint/style/useNamingConvention: it's how it's named
Object.assign(window, { Alpine });
window.addEventListener("DOMContentLoaded", () => {
Alpine.start();
});
+53 -25
View File
@@ -1,36 +1,64 @@
import { Alpine } from "alpinejs";
export enum NotificationLevel { export enum NotificationLevel {
Error = "error", Error = "error",
Warning = "warning", Warning = "warning",
Success = "success", Success = "success",
} }
export function createNotification(message: string, level: NotificationLevel) { export interface NotificationPlugin {
const element = document.getElementById("quick-notifications"); /**
if (element === null) { * Add an error message to the notifications.
return false; */
} error: (message: string) => void;
return element.dispatchEvent( /**
new CustomEvent("quick-notification-add", { * Add a warning message to the notifications
detail: { text: message, tag: level }, */
}), warning: (message: string) => void;
); /**
* Add a success message to the notifications
*/
success: (message: string) => void;
/**
* Remove all notifications displayed on the page.
*/
clear: () => void;
/**
* Add multiple notifications at once.
* The added notifications can have different notification levels.
*/
addMany: (notifs: Notification[]) => void;
/**
* Return all notifications displayed on the page.
*/
getAll: () => Notification[];
} }
export function deleteNotifications() { export interface Notification {
const element = document.getElementById("quick-notifications"); tag: NotificationLevel;
if (element === null) { text: string;
return false;
}
return element.dispatchEvent(new CustomEvent("quick-notification-delete"));
} }
export function alpinePlugin() { Alpine.store("notifications", [] as Notification[]);
return {
error: (message: string) => createNotification(message, NotificationLevel.Error), function createNotification(message: string, level: NotificationLevel) {
warning: (message: string) => (Alpine.store("notifications") as Notification[]).push({ text: message, tag: level });
createNotification(message, NotificationLevel.Warning), }
success: (message: string) => function createManyNotifications(notifs: Notification[]) {
createNotification(message, NotificationLevel.Success), for (const notif of notifs) {
clear: () => deleteNotifications(), createNotification(notif.text, notif.tag);
}; }
}
export const notifications: NotificationPlugin = {
error: (message: string) => createNotification(message, NotificationLevel.Error),
warning: (message: string) => createNotification(message, NotificationLevel.Warning),
success: (message: string) => createNotification(message, NotificationLevel.Success),
clear: () => Alpine.store("notifications", []),
addMany: (notifs: Notification[]) => createManyNotifications(notifs),
getAll: () => Alpine.store("notifications") as Notification[],
};
export function notificationsPlugin(GlobalAlpine: Alpine) {
GlobalAlpine.magic("notifications", () => ({ ...notifications }));
} }
+1 -1
View File
@@ -37,7 +37,7 @@
<script src="{{ url('javascript-catalog') }}"></script> <script src="{{ url('javascript-catalog') }}"></script>
<script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script> <script type="module" src="{{ static("bundled/core/navbar-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script> <script type="module" src="{{ static("bundled/core/components/include-index.ts") }}"></script>
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script> <script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/htmx-index.js') }}"></script> <script type="module" src="{{ static('bundled/htmx-index.js') }}"></script>
<script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script> <script type="module" src="{{ static('bundled/country-flags-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script> <script type="module" src="{{ static('bundled/core/tooltips-index.ts') }}"></script>
+5 -8
View File
@@ -1,16 +1,13 @@
<div id="quick-notifications" <div id="quick-notifications"
x-data='{ x-init='$notifications.addMany([
messages: [
{%- for message in messages -%} {%- for message in messages -%}
{%- if not message.extra_tags -%} {%- if not message.extra_tags -%}
{ tag: {{ message.tags|string|tojson }}, text: {{ message|string|tojson }} }, { tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor -%}
] ])'
}' >
@quick-notification-add="(e) => messages.push(e?.detail)" <template x-for="(message, index) in $notifications.getAll()">
@quick-notification-delete="messages = []">
<template x-for="(message, index) in messages">
<div class="alert" :class="`alert-${message.tag}`" x-transition> <div class="alert" :class="`alert-${message.tag}`" x-transition>
<span class="alert-main" x-text="message.text"></span> <span class="alert-main" x-text="message.text"></span>
<span class="clickable" @click="messages = messages.filter((item, i) => i !== index)"> <span class="clickable" @click="messages = messages.filter((item, i) => i !== index)">
+13 -10
View File
@@ -226,7 +226,7 @@
<button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button> <button type="button" onclick="checkbox_{{form_id}}(false);">{% trans %}Unselect All{% endtrans %}</button>
{% endmacro %} {% endmacro %}
{% macro update_notifications(messages, clear) %} {% macro update_notifications(messages, clear = True) %}
{# Update notification area from new messages sent by django backend {# Update notification area from new messages sent by django backend
This is useful when performing fragment swaps to keep messages up to date This is useful when performing fragment swaps to keep messages up to date
Without this, the fragment would need to take control of the notification area and Without this, the fragment would need to take control of the notification area and
@@ -236,16 +236,19 @@
messages: messages from django.contrib messages: messages from django.contrib
clear : optional boolean that controls if notifications should be cleared first. True is the default clear : optional boolean that controls if notifications should be cleared first. True is the default
#} #}
{% set clear = clear|default(true) %}
{% if messages %} {% if messages %}
<div x-init="() => { <div x-init='() => {
{% if clear %} {%- if clear -%}
$notifications.clear() $notifications.clear();
{% endif %} {%- endif -%}
{% for message in messages %} $notifications.addMany([
$notifications.{{ message.tags }}('{{ message }}') {%- for message in messages -%}
{% endfor %} {%- if not message.extra_tags -%}
}"></div> { tag: "{{ message.tags }}", text: {{ message|string|tojson }} },
{%- endif -%}
{%- endfor -%}
])
}'></div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
+1 -1
View File
@@ -54,7 +54,7 @@ class FragmentRenderer(Protocol):
) -> SafeString: ... ) -> SafeString: ...
class FragmentMixin(TemplateResponseMixin, ContextMixin): class FragmentMixin(TemplateResponseMixin, AllowFragment, ContextMixin):
"""Make a view buildable as a fragment that can be embedded in a template. """Make a view buildable as a fragment that can be embedded in a template.
Most fragments are used in two different ways : Most fragments are used in two different ways :
+2 -2
View File
@@ -35,8 +35,8 @@ les fichiers sont à mettre dans un dossier `static/bundled` de l'application à
Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `bundled/` comme prefix. Pour accéder au fichier, il faut utiliser `static` comme pour le reste mais en ajouter `bundled/` comme prefix.
```jinja ```jinja
{# Example pour ajouter sith/core/bundled/alpine-index.js #} {# Example pour ajouter sith/core/bundled/alpine-index.ts #}
<script type="module" src="{{ static('bundled/alpine-index.js') }}"></script> <script type="module" src="{{ static('bundled/alpine-index.ts') }}"></script>
<script type="module" src="{{ static('bundled/other-index.ts') }}"></script> <script type="module" src="{{ static('bundled/other-index.ts') }}"></script>
``` ```
@@ -32,5 +32,7 @@
</form> </form>
</div> </div>
<br> <br>
{{ update_notifications(messages) }} {% if is_fragment %}
{{ update_notifications(messages) }}
{% endif %}
</div> </div>
+1 -1
View File
@@ -32,7 +32,7 @@ class JsBundlerManifestEntry:
# because that's what the user types when importing statics and that's what django gives us # because that's what the user types when importing statics and that's what django gives us
# This is really similar to what we are doing in the bundler, it uses a similar algorithm # This is really similar to what we are doing in the bundler, it uses a similar algorithm
# Example: # Example:
# core/static/bundled/alpine-index.js -> bundled/alpine-index.js # core/static/bundled/alpine-index.ts -> bundled/alpine-index.ts
# core/static/bundled/components/include-index.ts -> core/static/bundled/components/include-index.ts # core/static/bundled/components/include-index.ts -> core/static/bundled/components/include-index.ts
def get_relative_src_name(name: str) -> str: def get_relative_src_name(name: str) -> str:
original_path = Path(name) original_path = Path(name)