Merge pull request #10465 from colinux/attestations-v2-prod

ETQ admin je peux activer la délivrance des attestations v2 (sous feature flag)
This commit is contained in:
Colin Darie 2024-06-24 08:56:31 +00:00 committed by GitHub
commit ccf5b255ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 724 additions and 258 deletions

View file

@ -623,40 +623,10 @@ textarea::placeholder {
color: $dark-grey;
}
@media (max-width: 62em) {
.padded-fixed-footer {
padding-top: 120px;
}
}
@media (min-width: 62em) {
.padded-fixed-footer {
padding-top: 60px;
}
}
[data-fr-theme="dark"] .fixed-footer {
border-top: 2px solid var(--background-action-low-blue-france-hover);
background-color: var(--background-action-low-blue-france);
}
.mandatory {
fill: currentColor;
}
.fixed-footer {
border-top: 2px solid $blue-france-500;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-top: $default-padding;
background-color: $white;
z-index: 2;
}
.fr-menu__list {
padding: $default-spacer;
overflow-y: auto;

View file

@ -0,0 +1,49 @@
@import "constants";
.fixed-footer {
border-top: 2px solid var(--border-plain-blue-france);
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-top: $default-padding;
background-color: var(--background-default-grey);
z-index: 2;
}
@media (max-width: 62em) {
.padded-fixed-footer {
padding-top: 120px;
}
}
@media (min-width: 62em) {
.padded-fixed-footer {
padding-top: 60px;
}
}
[data-fr-theme="dark"] .fixed-footer {
background-color: var(--background-action-low-blue-france);
}
.sticky-header {
padding-top: $default-padding;
padding-bottom: $default-padding;
&-container {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 800;
}
&-warning {
background-color: var(--background-contrast-warning);
}
p {
margin: 0;
}
}

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AutosaveNoticeComponent < ApplicationComponent
attr_reader :label_scope
def initialize(success:, label_scope:)
@success = success
@label_scope = label_scope
end
def success? = @success
def label
success? ? t(".#{label_scope}.saved") : t(".#{label_scope}.error")
end
end

View file

@ -0,0 +1,8 @@
---
en:
form:
saved: 'Form saved'
error: 'Form in error'
attestation:
saved: 'Attestation saved'
error: 'Attestation in error'

View file

@ -0,0 +1,8 @@
---
fr:
form:
saved: 'Formulaire enregistré'
error: 'Formulaire en erreur'
attestation:
saved: 'Attestation enregistrée'
error: 'Attestation en erreur'

View file

@ -0,0 +1,2 @@
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success?, "fr-badge--error" => !success?) }
= label

View file

@ -26,6 +26,8 @@ class Dsfr::CalloutComponent < ApplicationComponent
"fr-callout--brown-caramel"
when :success
"fr-callout--green-emeraude"
when :neutral
# default
else
"fr-background-alt--blue-france"
end

View file

@ -98,9 +98,7 @@ module Dsfr
})
end
if autoresize?
@opts.deep_merge!(data: { controller: 'autoresize' })
end
@opts.deep_merge!(data: { controller: token_list(@opts.dig(:data, :controller), 'autoresize' => autoresize?) })
@opts
end

View file

@ -5,6 +5,14 @@ class Procedure::Card::AttestationComponent < ApplicationComponent
private
def edit_attestation_path
if @procedure.attestation_templates_v2.any? || @procedure.feature_enabled?(:attestation_v2)
helpers.edit_admin_procedure_attestation_template_v2_path(@procedure)
else
helpers.edit_admin_procedure_attestation_template_path(@procedure)
end
end
def error_messages
@procedure.errors.messages_for(:attestation_template).to_sentence
end

View file

@ -1,5 +1,5 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to edit_admin_procedure_attestation_template_path(@procedure), class: 'fr-tile fr-enlarge-link' do
= link_to edit_attestation_path, class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between
- if @procedure.attestation_template&.activated?
%div

View file

@ -20,27 +20,9 @@ module Administrateurs
format.pdf do
html = render_to_string('/administrateurs/attestation_template_v2s/show', layout: 'attestation', formats: [:html])
headers = {
'Content-Type' => 'application/json',
'X-Request-Id' => Current.request_id
}
pdf = WeasyprintService.generate_pdf(html, procedure_id: @procedure.id, path: request.path, user_id: current_user.id)
body = {
html: html,
upstream_context: {
procedure_id: @procedure.id,
path: request.path,
user_id: current_user.id
}
}.to_json
response = Typhoeus.post(WEASYPRINT_URL, headers:, body:)
if response.success?
send_data(response.body, filename: 'attestation.pdf', type: 'application/pdf', disposition: 'inline')
else
raise StandardError.new("PDF Generation failed: #{response.return_code} #{response.status_message}")
end
send_data(pdf, filename: 'attestation.pdf', type: 'application/pdf', disposition: 'inline')
end
end
end
@ -77,6 +59,19 @@ module Administrateurs
def update
attestation_params = editor_params
# toggle activation
if @attestation_template.persisted? && @attestation_template.activated? != cast_bool(attestation_params[:activated])
@procedure.attestation_templates.v2.update_all(activated: attestation_params[:activated])
render :update && return
end
if @attestation_template.published? && should_edit_draft?
@attestation_template = @attestation_template.dup
@attestation_template.state = :draft
@attestation_template.procedure = @procedure
end
logo_file = attestation_params.delete(:logo)
signature_file = attestation_params.delete(:signature)
@ -88,15 +83,40 @@ module Administrateurs
attestation_params[:signature] = uninterlace_png(signature_file)
end
if !@attestation_template.update(attestation_params)
flash.alert = "Le modèle de lattestation contient des erreurs et n'a pas pu être enregistré. Corriger les erreurs."
end
@attestation_template.assign_attributes(attestation_params)
render :update
if @attestation_template.invalid?
flash.alert = "Lattestation contient des erreurs et n'a pas pu être enregistrée. Corriger les erreurs."
else
# - draft just published
if @attestation_template.published? && should_edit_draft?
published = @procedure.attestation_templates.published
@attestation_template.transaction do
were_published = published.destroy_all
@attestation_template.save!
flash.notice = were_published.any? ? "La nouvelle version de lattestation a été publiée." : "Lattestation a été publiée."
end
redirect_to edit_admin_procedure_attestation_template_v2_path(@procedure)
else
# - draft updated
# - or, attestation already published, without need for publication (draft procedure)
@attestation_template.save!
render :update
end
end
end
def create = update
def reset
@procedure.attestation_templates_v2.draft&.destroy_all
flash.notice = "Les modifications ont été réinitialisées."
redirect_to edit_admin_procedure_attestation_template_v2_path(@procedure)
end
private
def ensure_feature_active
@ -104,11 +124,19 @@ module Administrateurs
end
def retrieve_attestation_template
@attestation_template = @procedure.attestation_template_v2 || @procedure.build_attestation_template_v2(json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT)
v2s = @procedure.attestation_templates_v2
@attestation_template = v2s.find(&:draft?) || v2s.find(&:published?) || build_default_attestation
end
def build_default_attestation
state = should_edit_draft? ? :draft : :published
@procedure.build_attestation_template(version: 2, json_body: AttestationTemplate::TIPTAP_BODY_DEFAULT, activated: true, state:)
end
def should_edit_draft? = !@procedure.brouillon?
def editor_params
params.required(:attestation_template).permit(:official_layout, :label_logo, :label_direction, :tiptap_body, :footer, :logo, :signature, :activated)
params.required(:attestation_template).permit(:activated, :official_layout, :label_logo, :label_direction, :tiptap_body, :footer, :logo, :signature, :activated, :state)
end
end
end

View file

@ -112,7 +112,7 @@ module Administrateurs
revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } }
},
attestation_template_v1: [],
attestation_template_v2: [],
attestation_templates_v2: [],
initiated_mail: [],
received_mail: [],
closed_mail: [],

View file

@ -30,9 +30,10 @@ module Instructeurs
end
def apercu_attestation
@attestation = dossier.attestation_template.render_attributes_for(dossier: dossier)
render 'administrateurs/attestation_templates/show', formats: [:pdf]
send_data dossier.attestation_template.send(:build_pdf, dossier),
filename: 'attestation.pdf',
type: 'application/pdf',
disposition: 'inline'
end
def bilans_bdf

View file

@ -0,0 +1,43 @@
import { ApplicationController } from './application_controller';
export class StickyTopController extends ApplicationController {
// Ajusts top of sticky top components when there is a sticky header.
connect(): void {
const header = document.getElementById('sticky-header');
if (!header) {
return;
}
this.adjustTop(header);
window.addEventListener('resize', () => this.adjustTop(header));
this.listenHeaderMutations(header);
}
private listenHeaderMutations(header: HTMLElement) {
const config = { childList: true, subtree: true };
const callback: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
this.adjustTop(header);
break;
}
}
};
const observer = new MutationObserver(callback);
observer.observe(header, config);
}
private adjustTop(header: HTMLElement) {
const headerHeight = header.clientHeight;
if (headerHeight > 0) {
(this.element as HTMLElement).style.top = `${headerHeight + 8}px`;
}
}
}

View file

@ -2,11 +2,16 @@ class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern
belongs_to :procedure, inverse_of: :attestation_template_v2
belongs_to :procedure, inverse_of: :attestation_template
has_one_attached :logo
has_one_attached :signature
enum state: {
draft: 'draft',
published: 'published'
}
validates :title, tags: true, if: -> { procedure.present? && version == 1 }
validates :body, tags: true, if: -> { procedure.present? && version == 1 }
validates :json_body, tags: true, if: -> { procedure.present? && version == 2 }
@ -67,9 +72,10 @@ class AttestationTemplate < ApplicationRecord
}.freeze
def attestation_for(dossier)
attestation = Attestation.new(title: replace_tags(title, dossier, escape: false))
attestation = Attestation.new
attestation.title = replace_tags(title, dossier, escape: false) if version == 1
attestation.pdf.attach(
io: build_pdf(dossier),
io: StringIO.new(build_pdf(dossier)),
filename: "attestation-dossier-#{dossier.id}.pdf",
content_type: 'application/pdf',
# we don't want to run virus scanner on this file
@ -91,7 +97,7 @@ class AttestationTemplate < ApplicationRecord
end
def dup
attestation_template = AttestationTemplate.new(title: title, body: body, footer: footer, activated: activated)
attestation_template = super
ClonePiecesJustificativesService.clone_attachments(self, attestation_template)
attestation_template
end
@ -179,7 +185,7 @@ class AttestationTemplate < ApplicationRecord
if dossier.present?
# 2x faster this way than with `replace_tags` which would reparse text
used_tags = tiptap.used_tags_and_libelle_for(json.deep_symbolize_keys)
used_tags = TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false)
body = tiptap.to_html(json, substitutions)
@ -202,17 +208,41 @@ class AttestationTemplate < ApplicationRecord
end
def used_tags
used_tags_for(title) + used_tags_for(body)
if version == 2
json = json_body&.deep_symbolize_keys
TiptapService.used_tags_and_libelle_for(json.deep_symbolize_keys).map(&:first)
else
used_tags_for(title) + used_tags_for(body)
end
end
def build_pdf(dossier)
if version == 2
build_v2_pdf(dossier)
else
build_v1_pdf(dossier)
end
end
def build_v1_pdf(dossier)
attestation = render_attributes_for(dossier: dossier)
attestation_view = ApplicationController.render(
ApplicationController.render(
template: 'administrateurs/attestation_templates/show',
formats: :pdf,
assigns: { attestation: attestation }
)
end
StringIO.new(attestation_view)
def build_v2_pdf(dossier)
body = render_attributes_for(dossier:).fetch(:body)
html = ApplicationController.render(
template: '/administrateurs/attestation_template_v2s/show',
formats: [:html],
layout: 'attestation',
assigns: { attestation_template: self, body: body }
)
WeasyprintService.generate_pdf(html, { procedure_id: procedure.id, dossier_id: dossier.id })
end
end

View file

@ -257,7 +257,7 @@ module TagsSubstitutionConcern
def used_type_de_champ_tags(text_or_tiptap)
used_tags =
if text_or_tiptap.respond_to?(:deconstruct_keys) # hash pattern matching
TiptapService.new.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
TiptapService.used_tags_and_libelle_for(text_or_tiptap.deep_symbolize_keys)
else
used_tags_and_libelle_for(text_or_tiptap.to_s)
end

View file

@ -66,11 +66,10 @@ class ExportTemplate < ApplicationRecord
end
def render_attributes_for(content_for, dossier, attachment = nil)
tiptap = TiptapService.new
used_tags = tiptap.used_tags_and_libelle_for(content_for.deep_symbolize_keys)
used_tags = TiptapService.used_tags_and_libelle_for(content_for.deep_symbolize_keys)
substitutions = tags_substitutions(used_tags, dossier, escape: false, memoize: true)
substitutions['original-filename'] = attachment.filename.base if attachment
tiptap.to_path(content_for.deep_symbolize_keys, substitutions)
TiptapService.new.to_path(content_for.deep_symbolize_keys, substitutions)
end
def specific_tags

View file

@ -50,9 +50,9 @@ class Procedure < ApplicationRecord
has_one :module_api_carto, dependent: :destroy
has_many :attestation_templates, dependent: :destroy
has_one :attestation_template_v1, -> { AttestationTemplate.v1 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_many :attestation_templates_v2, -> { AttestationTemplate.v2 }, dependent: :destroy, class_name: "AttestationTemplate", inverse_of: :procedure
has_one :attestation_template, -> { order(Arel.sql("CASE WHEN version = '1' THEN 0 ELSE 1 END")) }, dependent: :destroy, inverse_of: :procedure
has_one :attestation_template, -> { published }, dependent: :destroy, inverse_of: :procedure
belongs_to :parent_procedure, class_name: 'Procedure', optional: true
belongs_to :canonical_procedure, class_name: 'Procedure', optional: true

View file

@ -1,18 +1,6 @@
class TiptapService
def to_html(node, substitutions = {})
return '' if node.nil?
children(node[:content], substitutions, 0)
end
def to_path(node, substitutions = {})
return '' if node.nil?
children_path(node[:content], substitutions)
end
# NOTE: node must be deep symbolized keys
def used_tags_and_libelle_for(node, tags = Set.new)
def self.used_tags_and_libelle_for(node, tags = Set.new)
case node
in type: 'mention', attrs: { id:, label: }, **rest
tags << [id, label]
@ -25,6 +13,18 @@ class TiptapService
tags
end
def to_html(node, substitutions = {})
return '' if node.nil?
children(node[:content], substitutions, 0)
end
def to_path(node, substitutions = {})
return '' if node.nil?
children_path(node[:content], substitutions)
end
private
def initialize

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class WeasyprintService
def self.generate_pdf(html, options = {})
headers = {
'Content-Type' => 'application/json',
'X-Request-Id' => Current.request_id
}
body = {
html:,
upstream_context: options
}.to_json
response = Typhoeus.post(WEASYPRINT_URL, headers:, body:)
if response.success?
response.body
else
raise StandardError, "PDF Generation failed: #{response.code} #{response.status_message}"
end
end
end

View file

@ -1,2 +0,0 @@
- success = local_assigns.fetch(:success, true)
#autosave-notice.fr-badge.fr-badge--sm{ class: class_names("fr-badge--success" => success, "fr-badge--error" => !success) }= success ? t(".form_saved") : t(".form_error")

View file

@ -0,0 +1,9 @@
.fr-container
.fr-grid-row.fr-grid-row--middle.fr-pb-3v
.fr-col-12.fr-col-md-4
= link_to admin_procedure_path(id: procedure), class: 'fr-link' do
%span.fr-icon-arrow-left-line.fr-icon--sm
Revenir à lécran de gestion
.fr-col-12.fr-col-md-8.text-right
%span#autosave-notice

View file

@ -0,0 +1,21 @@
.sticky-header.sticky-header-warning
.fr-container
%p.flex.justify-between.align-center.fr-text-default--warning
%span
= dsfr_icon("fr-icon-warning-fill fr-mr-1v")
- if @procedure.attestation_templates.many?
Les modifications effectuées ne seront appliquées quà la prochaine publication.
- else
Lattestation ne sera délivrée quaprès sa publication.
%span.no-wrap
- if @procedure.attestation_templates.many?
= link_to reset_admin_procedure_attestation_template_v2_path(@procedure), class: "fr-btn fr-btn--secondary fr-ml-2w", method: :post do
Réinitialiser les modifications
%button.fr-btn.fr-ml-2w{ form: "attestation-template", name: field_name(:attestation_template, :state), value: "published",
data: { 'disable-with': "Publication en cours…", controller: 'autosave-submit' } }
- if @procedure.attestation_templates.many?
Publier les modifications
- else
Publier

View file

@ -4,7 +4,8 @@
['Attestation']] }
= render NestedForms::FormOwnerComponent.new
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure), html: { multipart: true },
= form_for @attestation_template, url: admin_procedure_attestation_template_v2_path(@procedure),
html: { multipart: true , id: "attestation-template" },
data: { turbo: 'true',
controller: 'autosubmit attestation',
autosubmit_debounce_delay_value: 1000,
@ -19,11 +20,12 @@
tout en respectant la charte de létat. Essayez-la et donnez-nous votre avis
en nous envoyant un email à #{mail_to(Current.contact_email, subject: "Feedback attestation v2")}.
%br
%strong Les attestations délivrées suivent encore lancien format :
lactivation des attestations basées sur ce format sera bientôt disponible.
%br
- if !@procedure.feature_enabled?(:attestation_v2)
%strong Les attestations délivrées suivent encore lancien format :
lactivation des attestations basées sur ce format sera bientôt disponible.
%br
= link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure))
= link_to("Suivez ce lien pour revenir aux attestations actuellement délivrées", edit_admin_procedure_attestation_template_path(@procedure))
.fr-grid-row.fr-grid-row--gutters
.fr-col-12.fr-col-lg-7
@ -34,13 +36,23 @@
Lattestation est émise au moment où un dossier est accepté, elle est jointe à lemail daccusé dacceptation.
Elle est également disponible au téléchargement depuis lespace personnel de lusager.
.fr-fieldset__element
= render Dsfr::CalloutComponent.new(title: "Activation de la délivrance de lattestation", theme: :neutral) do |c|
- c.with_html_body do
.fr-toggle.fr-toggle--label-left
= f.check_box :activated, class: "fr-toggle__input", id: dom_id(@attestation_template, :activated)
%label.fr-toggle__label{ for: dom_id(@attestation_template, :activated),
data: { fr_checked_label: "Activée", fr_unchecked_label: "Désactivée" } }
Activer cette option permet la délivrance automatique de lattestation dès lacceptation du dossier.
Désactiver cette option arrête immédiatement lémission de nouvelles attestations.
.fr-fieldset__element
%h2.fr-h4 En-tête
.fr-fieldset__element
.fr-toggle.fr-toggle--label-left
= f.check_box :official_layout, class: "fr-toggle__input", id: dom_id(@attestation_template, :official_layout), data: { "attestation-target": "layoutToggle"}
%label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Activé", fr_unchecked_label: "Désactivé" } }
%label.fr-toggle__label{ for: dom_id(@attestation_template, :official_layout), data: { fr_checked_label: "Oui", fr_unchecked_label: "Non" } }
Je souhaite générer une attestation à la charte de létat (logo avec Marianne)
.fr-fieldset__element{ class: class_names("hidden" => !@attestation_template.official_layout?), data: { "attestation-target": 'logoMarianneLabelFieldset'} }
@ -77,10 +89,10 @@
%button.fr-btn.fr-btn--secondary.fr-btn--sm{ type: 'button', title: label, class: icon == :hidden ? "hidden" : "fr-icon-#{icon}", data: { action: 'click->tiptap#menuButton', tiptap_target: 'button', tiptap_action: action } }
= label
#editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: dom_id(f.object, "json-body-messages")} }
#editor.tiptap-editor{ data: { tiptap_target: 'editor' }, aria: { describedby: "attestation-template-json-body-messages"} }
= f.hidden_field :tiptap_body, data: { tiptap_target: 'input' }
.fr-error-text{ id: dom_id(f.object, "json-body-messages"), class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
.fr-error-text{ id: "attestation-template-json-body-messages", class: class_names("hidden" => !f.object.errors.include?(:json_body)) }
- if f.object.errors.include?(:json_body)
= render partial: "shared/errors_list", locals: { object: f.object, attribute: :json_body }
@ -108,7 +120,7 @@
- c.with_hint { "Exemple: 20 avenue de Ségur, 75007 Paris" }
#preview-column.fr-col-12.fr-col-lg-5.fr-background-alt--blue-france
.sticky--top.fr-px-1w
.sticky--top.fr-px-1w{ data: { controller: "sticky-top" } }
.flex.justify-between.align-center
%h2.fr-h4 Aperçu
%p= link_to 'Prévisualiser en taille réelle', admin_procedure_attestation_template_v2_path(@procedure, format: :pdf), class: 'fr-link', target: '_blank', rel: 'noopener'
@ -117,21 +129,10 @@
Laperçu est mis à jour automatiquement après chaque modification.
Pour générer un aperçu fidèle avec tous les champs et les dates, créez-vous un dossier et acceptez-le : laperçu lutilisera.
.padded-fixed-footer
.fixed-footer
.fr-container
.fr-grid-row
.fr-col-12.fr-col-md-7
%ul.fr-btns-group.fr-btns-group--inline-md
%li
= link_to admin_procedure_path(id: @procedure), class: 'fr-btn fr-btn--secondary' do
%span.fr-icon-arrow-go-back-line.fr-icon--sm.fr-mr-1v
Revenir à la démarche
- if @procedure.feature_enabled?(:attestation_v2) && @attestation_template.draft?
- content_for(:sticky_header) do
= render partial: "sticky_header"
.fr-col-12.fr-col-md-5
-# .fr-toggle
-# = f.check_box :activated, class: "fr-toggle-input", disabled: true, id: dom_id(@attestation_template, :activated)
-# %label.fr-toggle__label{ for: dom_id(@attestation_template, :activated), data: { fr_checked_label: "Attestation activée", fr_unchecked_label: "Attestation désactivée" } }
.text-right
%span#autosave-notice
%p.fr-hint-text Lactivation de cette attestation sera bientôt disponible.
.padded-fixed-footer
.fixed-footer#fixed_footer
= render partial: "fixed_footer", locals: { procedure: @procedure }

View file

@ -1,5 +1,8 @@
- if @attestation_template.draft?
= turbo_stream.update "sticky-header", render(partial: "sticky_header")
= turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice', locals: { success: !@attestation_template.changed? })
= turbo_stream.replace('autosave-notice', render(AutosaveNoticeComponent.new(success: !@attestation_template.changed?, label_scope: :attestation)))
= turbo_stream.hide 'autosave-notice', delay: 15000
- if @attestation_template.logo_blob&.previously_new_record?
@ -10,7 +13,7 @@
= turbo_stream.update dom_id(@attestation_template, :signature_attachment) do
= render(Attachment::EditComponent.new(attached_file: @attestation_template.signature, direct_upload: false))
- body_id = dom_id(@attestation_template, "json-body-messages")
- body_id = "attestation-template-json-body-messages"
- if @attestation_template.errors.include?(:json_body)
= turbo_stream.update body_id do
= render partial: "shared/errors_list", locals: { object: @attestation_template, attribute: :json_body }

View file

@ -18,7 +18,7 @@
- unless flash.alert
= turbo_stream.show 'autosave-notice'
= turbo_stream.replace 'autosave-notice', render(partial: 'administrateurs/autosave_notice')
= turbo_stream.replace 'autosave-notice', render(AutosaveNoticeComponent.new(success: true, label_scope: :form))
= turbo_stream.hide 'autosave-notice', delay: 30000
- if @destroyed.present?

View file

@ -45,6 +45,9 @@
#beta
Env Test
#sticky-header.sticky-header-container
= content_for(:sticky_header)
= render partial: "layouts/header"
%main#contenu{ role: :main }
= render partial: "layouts/flash_messages"