feat(annotations): activate autosave

This commit is contained in:
Paul Chavard 2023-03-01 18:30:10 +01:00
parent ff10a03ffe
commit cbaa77fca7
24 changed files with 147 additions and 117 deletions

View file

@ -0,0 +1,15 @@
class Dossiers::AutosaveFooterComponent < ApplicationComponent
include ApplicationHelper
attr_reader :dossier
def initialize(dossier:, annotation:)
@dossier = dossier
@annotation = annotation
end
private
def annotation?
@annotation
end
end

View file

@ -0,0 +1,15 @@
---
en:
brouillon:
explanation: Your draft is automatically saved.
confirmation: Draft saved
error: Impossible to save the draft
en_construction:
explanation: Your file is automatically saved.
confirmation: File saved
error: Impossible to save the file
annotations:
explanation: Your annotations are automatically saved.
confirmation: Annotations saved
error: Impossible to save the annotations
more_information: More informations

View file

@ -0,0 +1,15 @@
---
fr:
brouillon:
explanation: Votre brouillon est automatiquement enregistré.
confirmation: Brouillon enregistré
error: Impossible denregistrer le brouillon
en_construction:
explanation: Votre dossier est automatiquement enregistré.
confirmation: Dossier enregistré
error: Impossible denregistrer le dossier
annotations:
explanation: Vos annotations sont automatiquement enregistrées.
confirmation: Annotations enregistré
error: Impossible denregistrer les annotations
more_information: En savoir plus

View file

@ -0,0 +1,37 @@
.autosave.autosave-state-idle{ data: { controller: 'autosave-status' } }
%p.autosave-explanation.fr-text--sm
%span.autosave-explanation-text
- if annotation?
= t('.annotations.explanation')
- elsif dossier.brouillon?
= t('.brouillon.explanation')
- else
= t('.en_construction.explanation')
- if !annotation?
= link_to t('.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
%p.autosave-status.succeeded
%span.autosave-icon.icon.accept
%span.autosave-label
- if annotation?
= t('.annotations.confirmation')
- elsif dossier.brouillon?
= t('.brouillon.confirmation')
- else
= t('.en_construction.confirmation')
- if !annotation?
= link_to t('.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
%p.autosave-status.failed
%span.autosave-icon ⚠️
%span.autosave-label
- if annotation?
= t('.annotations.error')
- elsif dossier.brouillon?
= t('.brouillon.error')
- else
= t('.en_construction.error')
%button.button.small.autosave-retry{ type: :button, data: { action: 'autosave-status#onClickRetryButton', autosave_status_target: 'retryButton' } }
%span.autosave-retry-label réessayer
%span.autosave-retrying-label enregistrement en cours…

View file

@ -1,6 +1,7 @@
class Dossiers::EditFooterComponent < ApplicationComponent class Dossiers::EditFooterComponent < ApplicationComponent
def initialize(dossier:) def initialize(dossier:, annotation:)
@dossier = dossier @dossier = dossier
@annotation = annotation
end end
private private
@ -9,6 +10,10 @@ class Dossiers::EditFooterComponent < ApplicationComponent
controller.current_user.owns?(@dossier) controller.current_user.owns?(@dossier)
end end
def annotation?
@annotation
end
def button_options def button_options
{ {
class: 'fr-btn fr-btn--sm', class: 'fr-btn fr-btn--sm',

View file

@ -1,12 +1,13 @@
.dossier-edit-sticky-footer .dossier-edit-sticky-footer
.send-dossier-actions-bar .send-dossier-actions-bar
= render partial: 'shared/dossiers/autosave', locals: { dossier: @dossier } = render Dossiers::AutosaveFooterComponent.new(dossier: @dossier, annotation: annotation?)
- if @dossier.can_transition_to_en_construction? - if !annotation? && @dossier.can_transition_to_en_construction?
= button_to t('.submit'), brouillon_dossier_url(@dossier), button_options = button_to t('.submit'), brouillon_dossier_url(@dossier), button_options
- if @dossier.brouillon? && !owner? - if @dossier.brouillon? && !owner?
.send-notice.invite-cannot-submit .send-notice.invite-cannot-submit
= t('.invite_notice') = t('.invite_notice')
- if !annotation?
= render partial: "shared/dossiers/submit_is_over", locals: { dossier: @dossier } = render partial: "shared/dossiers/submit_is_over", locals: { dossier: @dossier }

View file

@ -8,4 +8,4 @@
- if @champ.rebased_at.present? && @champ.rebased_at > (@seen_at || @champ.updated_at) && current_user.owns_or_invite?(@champ.dossier) - if @champ.rebased_at.present? && @champ.rebased_at > (@seen_at || @champ.updated_at) && current_user.owns_or_invite?(@champ.dossier)
%span.updated-at.highlighted %span.updated-at.highlighted
Le type de ce @champ où sa description a été modifiée par l'administration. Vérifier son contenu. Le type de ce champ ou sa description ont été modifiés par l'administration. Vérifier son contenu.

View file

@ -28,12 +28,7 @@ class EditableChamp::EditableChampComponent < ApplicationComponent
def stimulus_controller def stimulus_controller
if !@champ.block? && @champ.fillable? if !@champ.block? && @champ.fillable?
# This is an editable champ. Lets find what controllers it might need. # This is an editable champ. Lets find what controllers it might need.
controllers = [] controllers = ['autosave']
# This is a public champ it can have an autosave controller.
if @champ.public?
controllers << 'autosave'
end
# This is a dropdown champ. Activate special behaviours it might have. # This is a dropdown champ. Activate special behaviours it might have.
if @champ.simple_drop_down_list? || @champ.linked_drop_down_list? if @champ.simple_drop_down_list? || @champ.linked_drop_down_list?

View file

@ -60,10 +60,6 @@ class TypesDeChampEditor::ChampComponent < ApplicationComponent
TypeDeChamp.type_champs TypeDeChamp.type_champs
.keys .keys
# FIXME
# We can only refresh after update champs when autosave is enabled. And it is disabled for now in private forms.
# So for new we restrict champs that require refresh after update to public forms.
.filter { type_de_champ.public? || !TypeDeChamp.refresh_after_update?(_1) }
.filter(&method(:filter_type_champ)) .filter(&method(:filter_type_champ))
.filter(&method(:filter_featured_type_champ)) .filter(&method(:filter_featured_type_champ))
.filter(&method(:filter_block_type_champ)) .filter(&method(:filter_block_type_champ))

View file

@ -0,0 +1,23 @@
module TurboChampsConcern
extend ActiveSupport::Concern
private
def champs_to_turbo_update(params, champs)
champ_ids = params.keys.map(&:to_i)
to_update = champs.filter { _1.id.in?(champ_ids) && _1.refresh_after_update? }
to_show, to_hide = champs.filter(&:conditional?)
.partition(&:visible?)
.map { champs_to_one_selector(_1 - to_update) }
return to_show, to_hide, to_update
end
def champs_to_one_selector(champs)
champs
.map(&:input_group_id)
.map { |id| "##{id}" }
.join(',')
end
end

View file

@ -4,6 +4,7 @@ module Instructeurs
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
include CreateAvisConcern include CreateAvisConcern
include DossierHelper include DossierHelper
include TurboChampsConcern
include ActionController::Streaming include ActionController::Streaming
include Zipline include Zipline
@ -244,12 +245,14 @@ module Instructeurs
if dossier.champs_private_all.any?(&:changed?) if dossier.champs_private_all.any?(&:changed?)
dossier.last_champ_private_updated_at = Time.zone.now dossier.last_champ_private_updated_at = Time.zone.now
end end
dossier.save if !dossier.save(context: :annotations)
dossier.log_modifier_annotations!(current_instructeur) flash.now.alert = dossier.errors.full_messages
end
respond_to do |format| respond_to do |format|
format.html { redirect_to annotations_privees_instructeur_dossier_path(procedure, dossier) } format.turbo_stream do
format.turbo_stream @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_private_params.fetch(:champs_private_all_attributes), dossier.champs_private_all)
end
end end
end end

View file

@ -1,6 +1,7 @@
module Users module Users
class DossiersController < UserController class DossiersController < UserController
include DossierHelper include DossierHelper
include TurboChampsConcern
layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret]
@ -198,7 +199,7 @@ module Users
respond_to do |format| respond_to do |format|
format.html { render :brouillon } format.html { render :brouillon }
format.turbo_stream do format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all)
render(:update, layout: false) render(:update, layout: false)
end end
@ -216,7 +217,7 @@ module Users
respond_to do |format| respond_to do |format|
format.html { render :modifier } format.html { render :modifier }
format.turbo_stream do format.turbo_stream do
@to_show, @to_hide, @to_update = champs_to_turbo_update @to_show, @to_hide, @to_update = champs_to_turbo_update(champs_public_params.fetch(:champs_public_all_attributes), dossier.champs_public_all)
end end
end end
end end
@ -483,24 +484,6 @@ module Users
errors errors
end end
def champs_to_turbo_update
champ_ids = champs_public_params
.fetch(:champs_public_all_attributes)
.keys
.map(&:to_i)
to_update = dossier
.champs_public_all
.filter { _1.id.in?(champ_ids) && _1.refresh_after_update? }
to_show, to_hide = dossier
.champs_public_all
.filter(&:conditional?)
.partition(&:visible?)
.map { champs_to_one_selector(_1 - to_update) }
return to_show, to_hide, to_update
end
def ensure_ownership! def ensure_ownership!
if !current_user.owns?(dossier) if !current_user.owns?(dossier)
forbidden! forbidden!
@ -561,12 +544,5 @@ module Users
submit_validation_options submit_validation_options
end end
end end
def champs_to_one_selector(champs)
champs
.map(&:input_group_id)
.map { |id| "##{id}" }
.join(',')
end
end end
end end

View file

@ -21,8 +21,6 @@ module Mutations
end end
if annotation.save if annotation.save
dossier.log_modifier_annotation!(annotation, instructeur)
{ annotation: } { annotation: }
else else
{ errors: annotation.errors.full_messages } { errors: annotation.errors.full_messages }

View file

@ -45,11 +45,8 @@ export class AutosaveController extends ApplicationController {
this.#latestPromise = Promise.resolve(); this.#latestPromise = Promise.resolve();
this.onGlobal('autosave:retry', () => this.didRequestRetry()); this.onGlobal('autosave:retry', () => this.didRequestRetry());
this.on('change', (event) => this.onChange(event)); this.on('change', (event) => this.onChange(event));
if (this.saveOnInput) {
this.on('input', (event) => this.onInput(event)); this.on('input', (event) => this.onInput(event));
} }
}
disconnect() { disconnect() {
this.#abortController?.abort(); this.#abortController?.abort();
@ -86,8 +83,7 @@ export class AutosaveController extends ApplicationController {
this.debounce(this.enqueueAutosaveRequest, AUTOSAVE_DEBOUNCE_DELAY); this.debounce(this.enqueueAutosaveRequest, AUTOSAVE_DEBOUNCE_DELAY);
} else if ( } else if (
isSelectElement(target) || isSelectElement(target) ||
isCheckboxOrRadioInputElement(target) || isCheckboxOrRadioInputElement(target)
(!this.saveOnInput && isTextInputElement(target))
) { ) {
// Wait next tick so champs having JS can interact // Wait next tick so champs having JS can interact
// with form elements before extracting form data. // with form elements before extracting form data.
@ -138,10 +134,6 @@ export class AutosaveController extends ApplicationController {
}, AUTOSAVE_CONDITIONAL_SPINNER_DEBOUNCE_DELAY); }, AUTOSAVE_CONDITIONAL_SPINNER_DEBOUNCE_DELAY);
} }
private get saveOnInput() {
return !!this.form?.dataset.saveOnInput;
}
private didRequestRetry() { private didRequestRetry() {
if (this.#needsRetry) { if (this.#needsRetry) {
this.enqueueAutosaveRequest(); this.enqueueAutosaveRequest();

View file

@ -1062,16 +1062,6 @@ class Dossier < ApplicationRecord
end end
end end
def log_modifier_annotations!(instructeur)
champs_private.filter(&:value_previously_changed?).each do |champ|
log_dossier_operation(instructeur, :modifier_annotation, champ)
end
end
def log_modifier_annotation!(champ, instructeur)
log_dossier_operation(instructeur, :modifier_annotation, champ)
end
def demander_un_avis!(avis) def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis) log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end end

View file

@ -0,0 +1,8 @@
- if @to_show.present?
= turbo_stream.show_all(@to_show)
- if @to_hide.present?
= turbo_stream.hide_all(@to_hide)
- @to_update.each do |champ|
= fields_for champ.input_name, champ do |form|
= turbo_stream.replace champ.input_group_id do
= render EditableChamp::EditableChampComponent.new champ:, form:

View file

@ -1,25 +0,0 @@
.autosave.autosave-state-idle{ data: { controller: 'autosave-status' } }
%p.autosave-explanation.fr-text--sm
%span.autosave-explanation-text
- if dossier.brouillon?
= t('views.users.dossiers.autosave.draft_explanation')
- else
= t('views.users.dossiers.autosave.explanation')
= link_to t('views.users.dossiers.autosave.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
%p.autosave-status.succeeded
%span.autosave-icon.icon.accept
%span.autosave-label
- if dossier.brouillon?
= t('views.users.dossiers.autosave.draft_confirmation')
- else
= t('views.users.dossiers.autosave.confirmation')
= link_to t('views.users.dossiers.autosave.more_information'), t("links.common.faq.autosave_url"), class: 'autosave-more-infos fr-link fr-link--sm', **external_link_attributes
%p.autosave-status.failed
%span.autosave-icon ⚠️
%span.autosave-label Impossible denregistrer le brouillon
%button.button.small.autosave-retry{ type: :button, data: { action: 'autosave-status#onClickRetryButton', autosave_status_target: 'retryButton' } }
%span.autosave-retry-label réessayer
%span.autosave-retrying-label enregistrement en cours…

View file

@ -6,7 +6,7 @@
= render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier }
- if dossier.brouillon? - if dossier.brouillon?
- form_options = { url: brouillon_dossier_url(dossier), method: :patch, data: { save_on_input: true } } - form_options = { url: brouillon_dossier_url(dossier), method: :patch }
- else - else
- form_options = { url: modifier_dossier_url(dossier), method: :patch } - form_options = { url: modifier_dossier_url(dossier), method: :patch }
= render Attachment::DeleteFormComponent.new = render Attachment::DeleteFormComponent.new
@ -47,4 +47,4 @@
= fields_for champ.input_name, champ do |form| = fields_for champ.input_name, champ do |form|
= render EditableChamp::EditableChampComponent.new form: form, champ: champ = render EditableChamp::EditableChampComponent.new form: form, champ: champ
= render Dossiers::EditFooterComponent.new(dossier: dossier) = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: false)

View file

@ -7,9 +7,6 @@
= fields_for champ.input_name, champ do |form| = fields_for champ.input_name, champ do |form|
= render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: seen_at = render EditableChamp::EditableChampComponent.new form: form, champ: champ, seen_at: seen_at
- if !dossier.for_procedure_preview? = render Dossiers::EditFooterComponent.new(dossier: dossier, annotation: true)
.send-wrapper
= f.submit 'Sauvegarder', class: 'button primary send', data: { disable: true }
- else - else
%h2.empty-text Aucune annotation privée %h2.empty-text Aucune annotation privée

View file

@ -112,6 +112,7 @@ ignore_unused:
- 'helpers.page_entries_info.*' - 'helpers.page_entries_info.*'
- 'combo_search_component.result_slot_html.*' - 'combo_search_component.result_slot_html.*'
- 'combo_search_component.screen_reader_instructions' - 'combo_search_component.screen_reader_instructions'
- 'links.common.*'
# - '{devise,kaminari,will_paginate}.*' # - '{devise,kaminari,will_paginate}.*'
# - 'simple_form.{yes,no}' # - 'simple_form.{yes,no}'
# - 'simple_form.{placeholders,hints,labels}.*' # - 'simple_form.{placeholders,hints,labels}.*'

View file

@ -281,12 +281,6 @@ en:
users: users:
dossiers: dossiers:
archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months" archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months"
autosave:
explanation: Your file is automatically saved.
confirmation: File saved
draft_explanation: Your draft is automatically saved.
draft_confirmation: Draft saved
more_information: More informations
identite: identite:
identity_data: Identity data identity_data: Identity data
all_required: All fields are required. all_required: All fields are required.

View file

@ -277,12 +277,6 @@ fr:
users: users:
dossiers: dossiers:
archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire" archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire"
autosave:
explanation: Votre dossier est automatiquement enregistré.
confirmation: Dossier enregistré
draft_explanation: Votre brouillon est automatiquement enregistré.
draft_confirmation: Brouillon enregistré
more_information: En savoir plus
identite: identite:
identity_data: Données didentité identity_data: Données didentité
all_required: Tous les champs sont obligatoires. all_required: Tous les champs sont obligatoires.

View file

@ -24,7 +24,7 @@ describe EditableChamp::EditableChampComponent, type: :component do
context 'when a private champ' do context 'when a private champ' do
let(:champ) { create(:champ, dossier: dossier, private: true) } let(:champ) { create(:champ, dossier: dossier, private: true) }
it { expect(subject).to eq('') } it { expect(subject).to eq('autosave') }
end end
context 'when a dossier is en_construction' do context 'when a dossier is en_construction' do
@ -41,7 +41,7 @@ describe EditableChamp::EditableChampComponent, type: :component do
end end
context 'when a private dropdown champ' do context 'when a private dropdown champ' do
let(:controllers) { ['champ-dropdown'] } let(:controllers) { ['autosave', 'champ-dropdown'] }
let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) }
it { expect(subject).to eq(data) } it { expect(subject).to eq(data) }
@ -56,7 +56,7 @@ describe EditableChamp::EditableChampComponent, type: :component do
end end
context 'when a private dropdown champ' do context 'when a private dropdown champ' do
let(:controllers) { ['champ-dropdown'] } let(:controllers) { ['autosave', 'champ-dropdown'] }
let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) } let(:champ) { create(:champ_drop_down_list, dossier: dossier, private: true) }
it { expect(subject).to eq(data) } it { expect(subject).to eq(data) }

View file

@ -771,7 +771,7 @@ describe Instructeurs::DossiersController, type: :controller do
expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :annotations_privees) expect(controller.current_instructeur).to receive(:mark_tab_as_seen).with(dossier, :annotations_privees)
another_instructeur.follow(dossier) another_instructeur.follow(dossier)
Timecop.freeze(now) Timecop.freeze(now)
patch :update_annotations, params: params patch :update_annotations, params: params, format: :turbo_stream
champ_multiple_drop_down_list.reload champ_multiple_drop_down_list.reload
champ_linked_drop_down_list.reload champ_linked_drop_down_list.reload
@ -819,7 +819,7 @@ describe Instructeurs::DossiersController, type: :controller do
expect(champ_datetime.value).to eq('2019-12-21T13:17:00+01:00') expect(champ_datetime.value).to eq('2019-12-21T13:17:00+01:00')
expect(champ_repetition.champs.first.value).to eq('text') expect(champ_repetition.champs.first.value).to eq('text')
expect(dossier.reload.last_champ_private_updated_at).to eq(now) expect(dossier.reload.last_champ_private_updated_at).to eq(now)
expect(response).to redirect_to(annotations_privees_instructeur_dossier_path(dossier.procedure, dossier)) expect(response).to have_http_status(200)
} }
it 'updates the annotations' do it 'updates the annotations' do
@ -848,7 +848,7 @@ describe Instructeurs::DossiersController, type: :controller do
it { it {
expect(dossier.reload.last_champ_private_updated_at).to eq(nil) expect(dossier.reload.last_champ_private_updated_at).to eq(nil)
expect(response).to redirect_to(annotations_privees_instructeur_dossier_path(dossier.procedure, dossier)) expect(response).to have_http_status(200)
} }
end end
end end