Skip to content

Commit

Permalink
Implémentation de la modale sur l'espace référent (#1454)
Browse files Browse the repository at this point in the history
  • Loading branch information
christophehenry authored Nov 19, 2024
2 parents 831c540 + 636e161 commit 11d7cf9
Show file tree
Hide file tree
Showing 30 changed files with 886 additions and 933 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ django-pgtrigger = "~=4.11.0"
django-phonenumber-field = {version = "~=8.0", extras = ["phonenumberslite"]}
django-referrer-policy = "==1.0"
django-reverse-admin = "==2.9.6"
django-template-partials = "==24.4"
gunicorn = "==22.0.0"
markdown = "~=3.5"
metabasepy = "==1.12.0"
Expand Down
218 changes: 114 additions & 104 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions aidants_connect/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def getenv_bool(key: str, default_value: Optional[bool] = None) -> bool:
"django_otp.plugins.otp_totp",
"django_celery_beat",
"django_extensions",
"template_partials",
"importmap",
"pgtrigger",
"import_export",
Expand Down
4 changes: 4 additions & 0 deletions aidants_connect/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"api/habilitation/",
include("aidants_connect_habilitation.api.urls"),
),
path(
"api/web/",
include("aidants_connect_web.api.urls"),
),
]

if "test" in sys.argv:
Expand Down
51 changes: 51 additions & 0 deletions aidants_connect_common/presenters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Presenters are classes that present data to HTML templates from different data sources
"""

from abc import ABC, abstractmethod
from typing import Any

from django.db.models import Model


class GenericHabilitationRequestPresenter(ABC):
"""Presenter to use with habilitation/generic-habilitation-request-profile-card.html""" # noqa: E501

@property
@abstractmethod
def pk(self) -> Model:
"""Return the underlying object containing data"""
...

@property
def details_id(self) -> str | None:
"""<details>' `id` parameter"""
return None

@property
@abstractmethod
def edit_endpoint(self) -> str:
pass

@property
@abstractmethod
def full_name(self) -> str:
pass

@property
@abstractmethod
def email(self) -> str:
pass

@property
@abstractmethod
def details_fields(self) -> list[dict[str, Any]]:
pass

@property
def form(self) -> str:
"""
Use this to render a hidden for containing this object's data, if needed.
Use this with formsets.
"""
return ""
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ details.request-card-details * {
box-sizing: border-box;
}

details.request-card-details summary {
details.request-card-details[open] summary {
flex-wrap: nowrap;
height: 100%;
min-height: 4rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,24 @@ class ProfileEditModal extends BaseController {
* @param {String|Promise<String>} content Modal content as HTML string or promise that resolves into HTML string
* @param {String} id Id of the item being edited
*/
createAndShow ({title = undefined, content, id} = {}) {
async createAndShow ({title = undefined, content, id} = {}) {
this.idValue = id
if (content instanceof Promise) {
this.stateValue = MODAL_STATES.LOADING;
} else {
content = Promise.resolve(content);
}

content.then(html => {
this.contentTarget.innerHTML = html;
try {
this.contentTarget.innerHTML = await content;
this.idValue = id;
this.stateValue = MODAL_STATES.IDLE;
}).catch(() => {
} catch (e) {
console.error(e)
this.stateValue = MODAL_STATES.ERROR;
})

this.displayValue = true
} finally {
this.displayValue = true
}
}

onValidate (evt) {
Expand Down Expand Up @@ -203,16 +204,26 @@ class ProfileEditModal extends BaseController {
/**
* @property {String} idValue
* @property {String} enpointValue
* @property {String} additionalFormValue
* @property {ProfileEditModal} profileEditModalOutlet
*/
class ProfileEditCard extends Controller {
static values = {id: String, enpoint: String}
static values = {id: String, enpoint: String, additionalForm: {type: String, default: undefined}}
static outlets = ["profile-edit-modal"]

onEdit (elt) {
onEdit (evt) {
evt.preventDefault()
evt.stopPropagation()

const additionnalForm = document.querySelector(this.additionalFormValue);
let urlParams = "";
if (additionnalForm instanceof HTMLFormElement) {
urlParams = `?${new URLSearchParams(new FormData(additionnalForm))}`
}

this.profileEditModalOutlet.createAndShow({
id: this.idValue,
content: fetch(this.enpointValue).then(async response => {
content: fetch(`${this.enpointValue}${urlParams}`).then(async response => {
if (!response.ok) {
throw response.statusText
}
Expand All @@ -223,10 +234,8 @@ class ProfileEditCard extends Controller {

/** @returns {Promise<String|undefined>} */
async validate () {
return fetch(this.enpointValue, {
body: new FormData(this.profileEditModalOutlet.contentTarget.querySelector("form")),
method: "POST",
}).then(async response => {
const body = new FormData(this.profileEditModalOutlet.contentTarget.querySelector("form"))
return fetch(this.enpointValue, {body, method: "POST"}).then(async response => {
// Error in the form, we return the HTML so that modal can display it
if (response.status === 422) {
return await response.text()
Expand Down Expand Up @@ -260,5 +269,8 @@ class ProfileEditCard extends Controller {
}
}

aidantsConnectApplicationReady.then(/** @param {Stimulus.Application} app */ app => app.register("profile-edit-modal", ProfileEditModal))
aidantsConnectApplicationReady.then(/** @param {Stimulus.Application} app */ app => app.register("profile-edit-card", ProfileEditCard))
aidantsConnectApplicationReady.then(/** @param {Stimulus.Application} app */ app => {
document.querySelector("main #modal-dest").insertAdjacentHTML("beforeend", document.querySelector("template#profile-edit-modal-tpl").innerHTML)
app.register("profile-edit-modal", ProfileEditModal)
app.register("profile-edit-card", ProfileEditCard)
})
6 changes: 5 additions & 1 deletion aidants_connect_common/templates/forms/form.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{% load partials %}

{% partialdef form %}{{ form }}{% endpartialdef %}

<form method="post" action="{{ action }}">
{% csrf_token %}
{{ form }}
{% partial form %}
</form>
Original file line number Diff line number Diff line change
@@ -1,66 +1,155 @@
{% load ac_common ac_extras static %}
{% load ac_common ac_extras dsfr_tags partials static %}

{# Prevent messing with the absolute positionning of the design #}
{# See https://stackoverflow.com/questions/17115344/absolute-positioning-ignoring-padding-of-parent #}
<div
id="profile-edit-card-{{ habilitation_request.req.pk }}"
class="fr-col-12 fr-col-md-6 request-card"
{% block container_args %}
{% if habilitation_request.edit_endpoint %}
data-controller="profile-edit-card"
data-profile-edit-card-id-value="{{ habilitation_request.req.pk }}"
data-profile-edit-card-enpoint-value="{{ habilitation_request.edit_endpoint }}"
data-profile-edit-card-profile-edit-modal-outlet="#profile-edit-modal"
{% comment %}
DOCUMENTATIION:
Use this template with aidants_connect_common.presenters.GenericHabilitationRequestPresenter.
Implement it, then provide a `objects` variable in context thats an iterable of GenericHabilitationRequestPresenter.
Use `habilitation-profile-card` partial to render one presenter.

See https://github.com/carltongibson/django-template-partials
{% endcomment %}

{% partialdef habilitation-profile-card %}
{# Prevent messing with the absolute positionning of the design #}
{# See https://stackoverflow.com/questions/17115344/absolute-positioning-ignoring-padding-of-parent #}
<div
id="profile-edit-card-{{ object.pk }}"
class="fr-col-12 fr-col-md-6 request-card"
{% if object.edit_endpoint %}
data-controller="profile-edit-card"
data-profile-edit-card-id-value="{{ object.pk }}"
data-profile-edit-card-enpoint-value="{{ object.edit_endpoint }}"
data-profile-edit-card-profile-edit-modal-outlet="#profile-edit-modal"
{% if additionnal_form_target %}
data-profile-edit-card-additional-form-value="{{ additionnal_form_target }}"
{% endif %}
{% endif %}
{% endblock container_args %}
>
<details
{% if habilitation_request.details_id %}id="{{ habilitation_request.details_id }}"{% endif %}
class="request-card-details"
name="generic-hab-request"
>
{% block details_introduction %}{% endblock %}
<summary
class="fr-grid-row fr-grid-row--middle fr-grid-row--gap-2v fr-tile--shadow fr-py-2v fr-px-4v"
title="{{ habilitation_request.user.full_name }} {{ habilitation_request.user.email }}"
<details
{% if object.details_id %}id="{{ object.details_id }}"{% endif %}
class="request-card-details"
name="generic-hab-request"
>
<img src="{% static "images/avatar.svg" %}" width="36" height="36" alt="" />
<span class="fr-grid-row--center fr-text--overflow-hidden spacer">
<p class="fr-m-0 fr-text--bold">{{ habilitation_request.user.full_name }}</p>
{% block details_subtitles %}
{% if habilitation_request.user.email %}
<p class="details-summary-header fr-m-0 fr-text--overflow-hidden">{{ habilitation_request.user.email }}</p>
<summary
class="fr-grid-row fr-grid-row--middle fr-grid-row--gap-2v fr-tile--shadow fr-py-2v fr-px-4v"
title="{{ object.full_name }} {{ object.email }}"
>
{{ object.form|safe }}
<img src="{% static "images/avatar.svg" %}" width="36" height="36" alt="" />
<span class="fr-grid-row--center fr-text--overflow-hidden spacer">
<p class="fr-m-0 fr-text--bold">{{ object.full_name }}</p>
{% if object.email %}
<p class="details-summary-header fr-m-0 fr-text--overflow-hidden">{{ object.email }}</p>
{% endif %}
{% endblock details_subtitles %}
</span>
<span
type="button"
class="fr-btn fr-btn--tertiary fr-btn--sm fr-icon-arrow-up-s-line"
aria-hidden="true"
></span>
</summary>
<div class="details-content fr-p-4v fr-tile--shadow">
<section class="user-informations">
{% for field in habilitation_request.user.details_fields %}
<section class="fr-my-4v">
<p class="fr-text--sm fr-text-mention--grey">{{ field.label }}</p>
{{ field.value }}
</section>
{% endfor %}
</section>
<ul class="fr-btns-group fr-btns-group--sm fr-btns-group--right fr-btns-group--inline fr-mb-n4v">
<li>
{% block action_buttons %}
<button
id="edit-button-{{ habilitation_request.obj.pk }}"
class="fr-btn fr-btn--tertiary fr-btn--icon fr-icon-edit-fill"
data-action="profile-edit-card#onEdit:prevent:stop"
>
Éditer
</button>
{% endblock action_buttons %}
</li>
</ul>
</div>
</details>
</div>
<span
type="button"
class="fr-btn fr-btn--tertiary fr-btn--sm fr-icon-arrow-up-s-line"
aria-hidden="true"
></span>
</summary>
<div class="details-content fr-p-4v fr-tile--shadow">
<section class="user-informations">
{% for field in object.details_fields %}
<section class="fr-my-4v">
<p class="fr-text--sm fr-text-mention--grey">{{ field.label }}</p>
{{ field.value }}
</section>
{% endfor %}
</section>
<ul class="fr-btns-group fr-btns-group--sm fr-btns-group--right fr-btns-group--inline fr-mb-n4v">
<li>
{% block action_buttons %}
<button
id="edit-button-{{ object.pk }}"
class="fr-btn fr-btn--tertiary fr-btn--icon fr-icon-edit-fill"
data-action="profile-edit-card#onEdit"
>
Éditer
</button>
{% endblock action_buttons %}
</li>
</ul>
</div>
</details>
</div>
{% endpartialdef %}

<section class="fr-grid-row fr-grid-row--gutters" {{ profile_cards_wrapper_attrs|default:""|safe }}>
<link href="{% static 'css/generic-habilitation-request-profile-card.css' %}" rel="stylesheet">

{% for object in objects %}
{% partial habilitation-profile-card %}
{% endfor %}

<template id="profile-edit-modal-tpl">
<button
data-fr-opened="false"
aria-controls="profile-edit-modal"
{% hidden %}
>{# Regression: https://github.com/GouvernementFR/dsfr/issues/728 #}</button>
<dialog
id="profile-edit-modal"
class="fr-modal"
aria-labelledby="profile-edit-modal-title"
role="alertdialog"
data-controller="profile-edit-modal"
data-action="dsfr.conceal->profile-edit-modal#onConceal"
data-profile-edit-modal-profile-edit-card-outlet=".request-card"
>
<div class="fr-container fr-container--fluid fr-container-md">
<div class="fr-grid-row fr-grid-row--center">
<div class="fr-col-12 fr-col-md-8 fr-col-lg-6">
<div class="fr-modal__body">
<div class="fr-modal__header">
<button class="fr-btn--close fr-btn" aria-controls="profile-edit-modal">Fermer</button>
</div>
<div class="fr-modal__content">
<h1
id="profile-edit-modal-title"
class="fr-modal__title sr-only"
data-profile-edit-modal-target="title"
></h1>
<section data-profile-edit-modal-target="content">

</section>
<img
src="{% static 'images/icons/AC-loader.svg' %}"
alt="Chargement, veuillez patienter…"
class="loader"
{% hidden %}
data-profile-edit-modal-target="loader"
/>
<section data-profile-edit-modal-target="error" {% hidden %}>
{% mailto SUPPORT_EMAIL as contact_email %}
{% dsfr_alert title="Une erreur sʼest produite" content=contact_email|strfmt:"Quelque-chose sʼest mal passé de notre côté. Ce n'est pas de votre faute. Veuillez fermer la fenêtre et réessayer. Si le problème persiste, veuillez nous contacter à {}" type="error" %}
</section>
</div>
<div class="fr-modal__footer" data-profile-edit-modal-target="footer" {% hidden %}>
<div class="fr-btns-group fr-btns-group--right fr-btns-group--inline-lg fr-btns-group--icon-left">
<button
id="profile-edit-suppress"
class="fr-btn fr-btn--secondary fr-btn--warning fr-btn--icon-left fr-icon-delete-bin-fill"
data-action="profile-edit-modal#onDelete:stop:prevent"
data-profile-edit-modal-target="footerButton"
>
Supprimer cet aidant
</button>
<button
id="profile-edit-submit"
class="fr-btn fr-btn--icon-left fr-icon-pencil-fill"
data-action="profile-edit-modal#onValidate:stop:prevent"
data-profile-edit-modal-target="footerButton"
>
Valider les modifications
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</dialog>
</template>
<script type="module" src="{% static 'js/generic-habilitation-request-profile-card.mjs' %}"></script>
</section>
Loading

0 comments on commit 11d7cf9

Please sign in to comment.