From 220db1f30ea70817be8ac5e3113a661ed94eee61 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 21 Apr 2022 17:29:42 +0200 Subject: [PATCH 01/31] fix(sentry#3144617191): render filesize to follow archive nicely --- app/dashboards/archive_dashboard.rb | 15 ++++----------- app/fields/attachment_field.rb | 3 ++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/dashboards/archive_dashboard.rb b/app/dashboards/archive_dashboard.rb index 50d53ea38..a18e8790b 100644 --- a/app/dashboards/archive_dashboard.rb +++ b/app/dashboards/archive_dashboard.rb @@ -12,7 +12,7 @@ class ArchiveDashboard < Administrate::BaseDashboard created_at: Field::DateTime, updated_at: Field::DateTime, status: Field::String, - file: Field::HasOne + file: AttachmentField }.freeze # COLLECTION_ATTRIBUTES @@ -24,7 +24,8 @@ class ArchiveDashboard < Administrate::BaseDashboard :id, :created_at, :updated_at, - :status + :status, + :file ].freeze # SHOW_PAGE_ATTRIBUTES @@ -33,14 +34,6 @@ class ArchiveDashboard < Administrate::BaseDashboard :id, :created_at, :updated_at, - :status, - :file + :status ].freeze - - # Overwrite this method to customize how users are displayed - # across all pages of the admin dashboard. - # - def display_resource(archive) - "Archive : #{archive&.file.&byte_size}" - end end diff --git a/app/fields/attachment_field.rb b/app/fields/attachment_field.rb index 2022fb355..4a09d2d7e 100644 --- a/app/fields/attachment_field.rb +++ b/app/fields/attachment_field.rb @@ -1,8 +1,9 @@ require "administrate/field/base" class AttachmentField < Administrate::Field::Base + include ActionView::Helpers::NumberHelper def to_s - data.filename.to_s + "#{data.filename} (#{number_to_human_size(data.byte_size)})" end def blob_path From 55d6b787c82ca1c2a153b9919704c34887e04076 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 20 Apr 2022 14:17:40 +0200 Subject: [PATCH 02/31] feat(js): add turbo --- Gemfile | 1 + Gemfile.lock | 3 +++ app/javascript/packs/application.js | 2 ++ package.json | 1 + yarn.lock | 18 ++++++++++++++++++ 5 files changed, 25 insertions(+) diff --git a/Gemfile b/Gemfile index 4d6db2cab..417367210 100644 --- a/Gemfile +++ b/Gemfile @@ -84,6 +84,7 @@ gem 'sib-api-v3-sdk' gem 'skylight' gem 'spreadsheet_architect' gem 'strong_migrations' # lint database migrations +gem 'turbo-rails' gem 'typhoeus' gem 'warden' gem 'webpacker' diff --git a/Gemfile.lock b/Gemfile.lock index ae2bd915b..b3fb8c66d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -723,6 +723,8 @@ GEM timecop (0.9.4) timeout (0.1.1) ttfunk (1.7.0) + turbo-rails (0.8.3) + rails (>= 6.0.0) typhoeus (1.4.0) ethon (>= 0.9.0) tzinfo (2.0.4) @@ -899,6 +901,7 @@ DEPENDENCIES spring-commands-rspec strong_migrations timecop + turbo-rails typhoeus vcr warden diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 89b5c3e7a..cedb6d332 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -3,6 +3,7 @@ import Rails from '@rails/ujs'; import * as ActiveStorage from '@rails/activestorage'; import 'whatwg-fetch'; // window.fetch polyfill import { Application } from '@hotwired/stimulus'; +import { Turbo } from '@hotwired/turbo-rails'; import '../shared/page-update-event'; import '../shared/activestorage/ujs'; @@ -89,6 +90,7 @@ const DS = { // Start Rails helpers Rails.start(); ActiveStorage.start(); +Turbo.session.drive = false; const Stimulus = Application.start(); Stimulus.register('react', ReactController); diff --git a/package.json b/package.json index 1b064f9d4..8d68f6920 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@headlessui/react": "^1.5.0", "@heroicons/react": "^1.0.6", "@hotwired/stimulus": "^3.0.1", + "@hotwired/turbo-rails": "^7.1.1", "@mapbox/mapbox-gl-draw": "^1.3.0", "@popperjs/core": "^2.11.4", "@rails/actiontext": "^6.1.4-1", diff --git a/yarn.lock b/yarn.lock index f47219e4c..bec7b1041 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1274,6 +1274,19 @@ resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.0.1.tgz#141f15645acaa3b133b7c247cad58ae252ffae85" integrity sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA== +"@hotwired/turbo-rails@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@hotwired/turbo-rails/-/turbo-rails-7.1.1.tgz#35c03b92b5c86f0137ed08bef843d955ec9bbe83" + integrity sha512-ZXpxUjCfkdbuXfoGrsFK80qsVzACs8xCfie9rt2jMTSN6o1olXVA0Nrk8u02yNEwSiVJm/4QSOa8cUcMj6VQjg== + dependencies: + "@hotwired/turbo" "^7.1.0" + "@rails/actioncable" "^7.0" + +"@hotwired/turbo@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@hotwired/turbo/-/turbo-7.1.0.tgz#27e44e0e3dc5bd1d4bda0766d579cf5a14091cd7" + integrity sha512-Q8kGjqwPqER+CtpQudbH+3Zgs2X4zb6pBAlr6NsKTXadg45pAOvxI9i4QpuHbwSzR2+x87HUm+rot9F/Pe8rxA== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -1949,6 +1962,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== +"@rails/actioncable@^7.0": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.0.2.tgz#69a6d999f4087e0537dd38fe0963db1f4305d650" + integrity sha512-G26maXW1Kx0LxQdmNNuNjQlRO/QlXNr3QfuwKiOKb5FZQGYe2OwtHTGXBAjSoiu4dW36XYMT/+L1Wo1Yov4ZXA== + "@rails/actiontext@^6.1.4-1": version "6.1.4" resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-6.1.4.tgz#ed8c7d2b68d66205301f4538ce65d04c48031f6b" From 4624fa141fd32645dfab1293fea3076744a4ce95 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 09:39:24 +0200 Subject: [PATCH 03/31] fix(turbo): use a safer render method --- app/models/concerns/mail_template_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/mail_template_concern.rb b/app/models/concerns/mail_template_concern.rb index d4368957e..d1e1f2d10 100644 --- a/app/models/concerns/mail_template_concern.rb +++ b/app/models/concerns/mail_template_concern.rb @@ -33,7 +33,7 @@ module MailTemplateConcern module ClassMethods def default_for_procedure(procedure) template_name = default_template_name_for_procedure(procedure) - rich_body = ActionController::Base.new.render_to_string(template: template_name) + rich_body = ActionController::Base.render template: template_name trix_rich_body = rich_body.gsub(/(?)\n/, '') new(subject: const_get(:DEFAULT_SUBJECT), rich_body: trix_rich_body, procedure: procedure) end From 69d5713c1913889a80e97f22eacf36a794a57d49 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 18:51:03 +0200 Subject: [PATCH 04/31] fix(turbo): fix anonymous controller RSpec test https://github.com/hotwired/hotwire-rails/issues/42 --- spec/controllers/concerns/devise_populated_resource_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/concerns/devise_populated_resource_spec.rb b/spec/controllers/concerns/devise_populated_resource_spec.rb index 32c01f8ac..3050f8324 100644 --- a/spec/controllers/concerns/devise_populated_resource_spec.rb +++ b/spec/controllers/concerns/devise_populated_resource_spec.rb @@ -1,6 +1,7 @@ describe DevisePopulatedResource, type: :controller do controller(Devise::PasswordsController) do include DevisePopulatedResource + layout false end let(:user) { create(:user) } From 0bd71ad51a35f8f8c26282acd659d5df1269bf21 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 14 Apr 2022 20:46:28 +0200 Subject: [PATCH 05/31] feat(turbo): add turbo event helpers --- app/assets/stylesheets/custom_reset.scss | 4 + app/helpers/turbo_stream_helper.rb | 35 +++++++ .../controllers/turbo_event_controller.ts | 91 +++++++++++++++++++ app/javascript/packs/application.js | 2 + app/views/layouts/_turbo_event.html.haml | 5 + app/views/layouts/application.html.haml | 2 + .../layouts/application.turbo_stream.haml | 6 ++ package.json | 3 +- yarn.lock | 5 + 9 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 app/helpers/turbo_stream_helper.rb create mode 100644 app/javascript/controllers/turbo_event_controller.ts create mode 100644 app/views/layouts/_turbo_event.html.haml create mode 100644 app/views/layouts/application.turbo_stream.haml diff --git a/app/assets/stylesheets/custom_reset.scss b/app/assets/stylesheets/custom_reset.scss index b59355b03..e3486d5c1 100644 --- a/app/assets/stylesheets/custom_reset.scss +++ b/app/assets/stylesheets/custom_reset.scss @@ -22,3 +22,7 @@ a { text-decoration: none; } + +turbo-events { + display: none; +} diff --git a/app/helpers/turbo_stream_helper.rb b/app/helpers/turbo_stream_helper.rb new file mode 100644 index 000000000..40699c66e --- /dev/null +++ b/app/helpers/turbo_stream_helper.rb @@ -0,0 +1,35 @@ +module TurboStreamHelper + def turbo_stream + TagBuilder.new(self) + end + + class TagBuilder < Turbo::Streams::TagBuilder + def dispatch(type, detail) + append_all('turbo-events', partial: 'layouts/turbo_event', locals: { type: type, detail: detail }) + end + + def show(target, delay: nil) + dispatch('dom:mutation', { action: :show, target: target, delay: delay }.compact) + end + + def show_all(targets, delay: nil) + dispatch('dom:mutation', { action: :show, targets: targets, delay: delay }.compact) + end + + def hide(target, delay: nil) + dispatch('dom:mutation', { action: :hide, target: target, delay: delay }.compact) + end + + def hide_all(targets, delay: nil) + dispatch('dom:mutation', { action: :hide, targets: targets, delay: delay }.compact) + end + + def focus(target) + dispatch('dom:mutation', { action: :focus, target: target }) + end + + def focus_all(targets) + dispatch('dom:mutation', { action: :focus, targets: targets }) + end + end +end diff --git a/app/javascript/controllers/turbo_event_controller.ts b/app/javascript/controllers/turbo_event_controller.ts new file mode 100644 index 000000000..8f959e5a7 --- /dev/null +++ b/app/javascript/controllers/turbo_event_controller.ts @@ -0,0 +1,91 @@ +import { Controller } from '@hotwired/stimulus'; +import invariant from 'tiny-invariant'; +import { z } from 'zod'; + +type Detail = Record; + +export class TurboEventController extends Controller { + static values = { + type: String, + detail: Object + }; + + declare readonly typeValue: string; + declare readonly detailValue: Detail; + + connect(): void { + this.globalDispatch(this.typeValue, this.detailValue); + this.element.remove(); + } + + private globalDispatch(type: string, detail: Detail): void { + this.dispatch(type, { + detail, + prefix: '', + target: document.documentElement + }); + } +} + +const MutationAction = z.enum(['show', 'hide', 'focus']); +type MutationAction = z.infer; +const Mutation = z.union([ + z.object({ + action: MutationAction, + delay: z.number().optional(), + target: z.string() + }), + z.object({ + action: MutationAction, + delay: z.number().optional(), + targets: z.string() + }) +]); +type Mutation = z.infer; + +addEventListener('dom:mutation', (event) => { + const detail = (event as CustomEvent).detail; + const mutation = Mutation.parse(detail); + mutate(mutation); +}); + +const Mutations: Record void> = { + hide: (mutation) => { + for (const element of findElements(mutation)) { + element.classList.add('hidden'); + } + }, + show: (mutation) => { + for (const element of findElements(mutation)) { + element.classList.remove('hidden'); + } + }, + focus: (mutation) => { + for (const element of findElements(mutation)) { + element.focus(); + } + } +}; + +function mutate(mutation: Mutation) { + const fn = Mutations[mutation.action]; + invariant(fn, `Could not find mutation ${mutation.action}`); + if (mutation.delay) { + setTimeout(() => fn(mutation), mutation.delay); + } else { + fn(mutation); + } +} + +function findElements( + mutation: Mutation +): Element[] { + if ('target' in mutation) { + const element = document.querySelector(`#${mutation.target}`); + invariant(element, `Could not find element with id ${mutation.target}`); + return [element]; + } else if ('targets' in mutation) { + return [...document.querySelectorAll(mutation.targets)]; + } + invariant(false, 'Could not find element'); +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index cedb6d332..79a11009e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -18,6 +18,7 @@ import { ReactController, registerComponents } from '../controllers/react_controller'; +import { TurboEventController } from '../controllers/turbo_event_controller'; import '../new_design/dropdown'; import '../new_design/form-validation'; @@ -94,6 +95,7 @@ Turbo.session.drive = false; const Stimulus = Application.start(); Stimulus.register('react', ReactController); +Stimulus.register('turbo-event', TurboEventController); // Expose globals window.DS = window.DS || DS; diff --git a/app/views/layouts/_turbo_event.html.haml b/app/views/layouts/_turbo_event.html.haml new file mode 100644 index 000000000..e7ddff930 --- /dev/null +++ b/app/views/layouts/_turbo_event.html.haml @@ -0,0 +1,5 @@ +%turbo-event{ data: { + controller: 'turbo-event', + turbo_event_type_value: type, + turbo_event_detail_value: detail.to_json +} } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 52f4934b4..ed9d71b76 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -41,3 +41,5 @@ = content_for(:footer) = yield :charts_js + + %turbo-events diff --git a/app/views/layouts/application.turbo_stream.haml b/app/views/layouts/application.turbo_stream.haml new file mode 100644 index 000000000..343964e1f --- /dev/null +++ b/app/views/layouts/application.turbo_stream.haml @@ -0,0 +1,6 @@ +- if flash.any? + = turbo_stream.replace 'flash_messages', partial: 'layouts/flash_messages' + = turbo_stream.hide 'flash_messages', delay: 10000 + - flash.clear + += yield diff --git a/package.json b/package.json index 8d68f6920..7bef976e0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "use-debounce": "^5.2.0", "webpack": "^4.46.0", "webpack-cli": "^3.3.12", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.0.0", + "zod": "^3.14.4" }, "devDependencies": { "@2fd/graphdoc": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index bec7b1041..adbc387fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14032,3 +14032,8 @@ zip-stream@^4.1.0: archiver-utils "^2.1.0" compress-commons "^4.1.0" readable-stream "^3.6.0" + +zod@^3.14.4: + version "3.14.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.14.4.tgz#e678fe9e5469f4663165a5c35c8f3dc66334a5d6" + integrity sha512-U9BFLb2GO34Sfo9IUYp0w3wJLlmcyGoMd75qU9yf+DrdGA4kEx6e+l9KOkAlyUO0PSQzZCa3TR4qVlcmwqSDuw== From 8d0383c6f89353feeac174e05659780559547e6e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 12:27:38 +0200 Subject: [PATCH 06/31] refactor(turbo): use turbo in root controller --- app/controllers/root_controller.rb | 2 +- app/views/layouts/_outdated_browser_banner.html.haml | 2 +- app/views/root/dismiss_outdated_browser.turbo_stream.haml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 app/views/root/dismiss_outdated_browser.turbo_stream.haml diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 8a35c774c..0e1335ef3 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -84,7 +84,7 @@ class RootController < ApplicationController respond_to do |format| format.html { redirect_back(fallback_location: root_path) } - format.js { render js: helpers.remove_element('#outdated-browser-banner') } + format.turbo_stream end end diff --git a/app/views/layouts/_outdated_browser_banner.html.haml b/app/views/layouts/_outdated_browser_banner.html.haml index 5efc631c2..e1ad0350a 100644 --- a/app/views/layouts/_outdated_browser_banner.html.haml +++ b/app/views/layouts/_outdated_browser_banner.html.haml @@ -16,6 +16,6 @@ %br Certaines parties du site ne fonctionneront pas correctement. .site-banner-actions - = button_to 'Ignorer', dismiss_outdated_browser_path, method: :post, remote: true, class: 'button btn', title: 'Ne plus afficher cet avertissement pendant une semaine' + = button_to 'Ignorer', dismiss_outdated_browser_path, method: :post, form: { data: { turbo: true } }, class: 'button btn', title: 'Ne plus afficher cet avertissement pendant une semaine' %a.btn.button.primary{ href: "https://browser-update.org/fr/update.html", target: "_blank", rel: "noopener" } Mettre à jour mon navigateur diff --git a/app/views/root/dismiss_outdated_browser.turbo_stream.haml b/app/views/root/dismiss_outdated_browser.turbo_stream.haml new file mode 100644 index 000000000..e95546cc5 --- /dev/null +++ b/app/views/root/dismiss_outdated_browser.turbo_stream.haml @@ -0,0 +1 @@ += turbo_stream.remove('outdated-browser-banner') From cad63391dbb27b988f40dd6d2fe42012098facde Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 12:28:29 +0200 Subject: [PATCH 07/31] refactor(turbo): use turbo in procedure administrateurs controller --- .../_add_admin_form.html.haml | 4 ++-- .../_administrateur.html.haml | 10 +++++----- .../procedure_administrateurs/create.js.haml | 9 --------- .../create.turbo_stream.haml | 3 +++ .../procedure_administrateurs/destroy.js.haml | 4 ---- .../destroy.turbo_stream.haml | 2 ++ .../procedure_administrateurs/index.html.haml | 4 ++-- .../procedure_administrateurs_controller_spec.rb | 11 ++++++----- 8 files changed, 20 insertions(+), 27 deletions(-) delete mode 100644 app/views/administrateurs/procedure_administrateurs/create.js.haml create mode 100644 app/views/administrateurs/procedure_administrateurs/create.turbo_stream.haml delete mode 100644 app/views/administrateurs/procedure_administrateurs/destroy.js.haml create mode 100644 app/views/administrateurs/procedure_administrateurs/destroy.turbo_stream.haml diff --git a/app/views/administrateurs/procedure_administrateurs/_add_admin_form.html.haml b/app/views/administrateurs/procedure_administrateurs/_add_admin_form.html.haml index c59f2f75d..3f744264a 100644 --- a/app/views/administrateurs/procedure_administrateurs/_add_admin_form.html.haml +++ b/app/views/administrateurs/procedure_administrateurs/_add_admin_form.html.haml @@ -1,7 +1,7 @@ = form_for procedure.administrateurs.new(user: User.new), url: { controller: 'procedure_administrateurs' }, - html: { class: 'form', id: "procedure-#{procedure.id}-new_administrateur" } , - remote: true do |f| + html: { class: 'form', id: "new_administrateur" }, + data: { turbo: true } do |f| = f.label :email do Ajouter un administrateur %p.notice Renseignez l’email d’un administrateur déjà enregistré sur #{APPLICATION_NAME} pour lui permettre de modifier « #{procedure.libelle} ». diff --git a/app/views/administrateurs/procedure_administrateurs/_administrateur.html.haml b/app/views/administrateurs/procedure_administrateurs/_administrateur.html.haml index ca8fb3310..dce0a1a83 100644 --- a/app/views/administrateurs/procedure_administrateurs/_administrateur.html.haml +++ b/app/views/administrateurs/procedure_administrateurs/_administrateur.html.haml @@ -1,4 +1,4 @@ -%tr{ id: "procedure-#{@procedure.id}-administrateur-#{administrateur.id}" } +%tr{ id: dom_id(administrateur) } %td= administrateur.email %td= try_format_datetime(administrateur.created_at) %td= administrateur.registration_state @@ -6,8 +6,8 @@ - if administrateur == current_administrateur C’est vous ! - else - = link_to 'Retirer', - admin_procedure_administrateur_path(@procedure, administrateur), + = button_to 'Retirer', + admin_procedure_administrateur_path(procedure, administrateur), method: :delete, - 'data-confirm': "Retirer « #{administrateur.email} » des administrateurs de « #{@procedure.libelle} » ?", - remote: true + class: 'button', + form: { data: { turbo: true, turbo_confirm: "Retirer « #{administrateur.email} » des administrateurs de « #{procedure.libelle} » ?" } } diff --git a/app/views/administrateurs/procedure_administrateurs/create.js.haml b/app/views/administrateurs/procedure_administrateurs/create.js.haml deleted file mode 100644 index 610fbd95b..000000000 --- a/app/views/administrateurs/procedure_administrateurs/create.js.haml +++ /dev/null @@ -1,9 +0,0 @@ -= render_flash(sticky: true) -- if @administrateur - = append_to_element("#procedure-#{@procedure.id}-administrateurs", - partial: 'administrateur', - locals: { administrateur: @administrateur }) - = render_to_element("#procedure-#{@procedure.id}-new_administrateur", - partial: 'add_admin_form', - outer: true, - locals: { procedure: @procedure }) diff --git a/app/views/administrateurs/procedure_administrateurs/create.turbo_stream.haml b/app/views/administrateurs/procedure_administrateurs/create.turbo_stream.haml new file mode 100644 index 000000000..b8af77c66 --- /dev/null +++ b/app/views/administrateurs/procedure_administrateurs/create.turbo_stream.haml @@ -0,0 +1,3 @@ +- if @administrateur.present? + = turbo_stream.append "administrateurs", partial: 'administrateur', locals: { procedure: @procedure, administrateur: @administrateur } + = turbo_stream.replace "new_administrateur", partial: 'add_admin_form', locals: { procedure: @procedure } diff --git a/app/views/administrateurs/procedure_administrateurs/destroy.js.haml b/app/views/administrateurs/procedure_administrateurs/destroy.js.haml deleted file mode 100644 index b56486490..000000000 --- a/app/views/administrateurs/procedure_administrateurs/destroy.js.haml +++ /dev/null @@ -1,4 +0,0 @@ -= render_flash(sticky: true) -- if @administrateur - = remove_element("#procedure-#{@procedure.id}-administrateur-#{@administrateur.id}") - diff --git a/app/views/administrateurs/procedure_administrateurs/destroy.turbo_stream.haml b/app/views/administrateurs/procedure_administrateurs/destroy.turbo_stream.haml new file mode 100644 index 000000000..4b4645f2d --- /dev/null +++ b/app/views/administrateurs/procedure_administrateurs/destroy.turbo_stream.haml @@ -0,0 +1,2 @@ +- if @administrateur.present? + = turbo_stream.remove(@administrateur) diff --git a/app/views/administrateurs/procedure_administrateurs/index.html.haml b/app/views/administrateurs/procedure_administrateurs/index.html.haml index d3545a511..9fbccb6bd 100644 --- a/app/views/administrateurs/procedure_administrateurs/index.html.haml +++ b/app/views/administrateurs/procedure_administrateurs/index.html.haml @@ -10,8 +10,8 @@ %th= 'Adresse email' %th= 'Enregistré le' %th= 'État' - %tbody{ id: "procedure-#{@procedure.id}-administrateurs" } - = render partial: 'administrateur', collection: @procedure.administrateurs.order('users.email') + %tbody#administrateurs + = render partial: 'administrateur', collection: @procedure.administrateurs.order('users.email'), locals: { procedure: @procedure } %tfoot %tr %th{ colspan: 4 } diff --git a/spec/controllers/administrateurs/procedure_administrateurs_controller_spec.rb b/spec/controllers/administrateurs/procedure_administrateurs_controller_spec.rb index 2094a8d54..15122c393 100644 --- a/spec/controllers/administrateurs/procedure_administrateurs_controller_spec.rb +++ b/spec/controllers/administrateurs/procedure_administrateurs_controller_spec.rb @@ -2,6 +2,7 @@ describe Administrateurs::ProcedureAdministrateursController, type: :controller let(:signed_in_admin) { create(:administrateur) } let(:other_admin) { create(:administrateur) } let(:procedure) { create(:procedure, administrateurs: [signed_in_admin, other_admin]) } + render_views before do sign_in(signed_in_admin.user) @@ -9,7 +10,7 @@ describe Administrateurs::ProcedureAdministrateursController, type: :controller describe '#destroy' do subject do - delete :destroy, params: { procedure_id: procedure.id, id: admin_to_remove.id }, format: :js, xhr: true + delete :destroy, params: { procedure_id: procedure.id, id: admin_to_remove.id }, format: :turbo_stream end context 'when removing another admin' do @@ -17,8 +18,8 @@ describe Administrateurs::ProcedureAdministrateursController, type: :controller it 'removes the admin from the procedure' do subject - expect(response.status).to eq(200) - expect(flash[:notice]).to be_present + expect(response).to have_http_status(:ok) + expect(subject.body).to include('alert-success') expect(admin_to_remove.procedures.reload).not_to include(procedure) end end @@ -28,8 +29,8 @@ describe Administrateurs::ProcedureAdministrateursController, type: :controller it 'denies the right for an admin to remove itself' do subject - expect(response.status).to eq(200) - expect(flash[:alert]).to be_present + expect(response).to have_http_status(:ok) + expect(subject.body).to include('alert-danger') expect(admin_to_remove.procedures.reload).to include(procedure) end end From 3e1bba7561d697940e80150b742c6f83f5691055 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 20 Apr 2022 15:17:49 +0200 Subject: [PATCH 08/31] doc(ux): clarifie le process pour l'ux research --- .github/ISSUE_TEMPLATE/description-de-probleme-ux.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/description-de-probleme-ux.md diff --git a/.github/ISSUE_TEMPLATE/description-de-probleme-ux.md b/.github/ISSUE_TEMPLATE/description-de-probleme-ux.md new file mode 100644 index 000000000..203ce73cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/description-de-probleme-ux.md @@ -0,0 +1,11 @@ +# contexte ; + +ex: ETQ usager, lorsque je dépose mon dossier, celui ci peut rester en brouillon sans que je comprenne qu'il n'est pas encore déposé au sens administratif + +# difficultés + +ex: sur ce même contexte, je ne comprends pas pourquoi ma démarche est bloquée + +# opportunités + +ex: améliorer le taux de conversion dépot de dossier brouillon > dépot de dossier en construction From a3c5bf58ac245258413a4c08658a3d81c5960727 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 21 Apr 2022 15:34:24 +0200 Subject: [PATCH 09/31] bug(ProcedureExportService.to_zip): use in_batches on Array --- .../services/procedure_export_service_spec.rb | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index 1021691b6..d85185d2a 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -1,10 +1,12 @@ require 'csv' describe ProcedureExportService do - describe 'to_data' do - let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } + let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } + let(:service) { ProcedureExportService.new(procedure, procedure.dossiers) } + + describe 'to_xlsx' do subject do - ProcedureExportService.new(procedure, procedure.dossiers) + service .to_xlsx .open { |f| SimpleXlsxReader.open(f.path) } end @@ -406,4 +408,15 @@ describe ProcedureExportService do end end end + + describe 'to_zip' do + subject do + service + .to_zip + end + + it 'does not raises in_batches' do + expect(subject).not_to raise_error(NoMethodError) + end + end end From 79c473ecf9f8f7c6312af8c3be6ef40f08aaab44 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 21 Apr 2022 17:02:23 +0200 Subject: [PATCH 10/31] fix(ProcedureExportService.to_zip): prebatch dossiers only for to_csv,to_xlsx,to_ods. also add spec around to_zip --- app/services/procedure_export_service.rb | 5 ++- .../services/procedure_export_service_spec.rb | 41 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/app/services/procedure_export_service.rb b/app/services/procedure_export_service.rb index 907426fd6..ffc99dbfd 100644 --- a/app/services/procedure_export_service.rb +++ b/app/services/procedure_export_service.rb @@ -3,16 +3,18 @@ class ProcedureExportService def initialize(procedure, dossiers) @procedure = procedure - @dossiers = dossiers.downloadable_sorted_batch + @dossiers = dossiers @tables = [:dossiers, :etablissements, :avis] + champs_repetables_options end def to_csv + @dossiers = @dossiers.downloadable_sorted_batch io = StringIO.new(SpreadsheetArchitect.to_csv(options_for(:dossiers, :csv))) create_blob(io, :csv) end def to_xlsx + @dossiers = @dossiers.downloadable_sorted_batch # We recursively build multi page spreadsheet io = @tables.reduce(nil) do |package, table| SpreadsheetArchitect.to_axlsx_package(options_for(table, :xlsx), package) @@ -21,6 +23,7 @@ class ProcedureExportService end def to_ods + @dossiers = @dossiers.downloadable_sorted_batch # We recursively build multi page spreadsheet io = StringIO.new(@tables.reduce(nil) do |spreadsheet, table| SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table, :ods), spreadsheet) diff --git a/spec/services/procedure_export_service_spec.rb b/spec/services/procedure_export_service_spec.rb index d85185d2a..d8211a8b2 100644 --- a/spec/services/procedure_export_service_spec.rb +++ b/spec/services/procedure_export_service_spec.rb @@ -410,13 +410,44 @@ describe ProcedureExportService do end describe 'to_zip' do - subject do - service - .to_zip + subject { service.to_zip } + context 'without files' do + it 'does not raises in_batches' do + expect { subject }.not_to raise_error(NoMethodError) + end + + it 'returns an empty blob' do + expect(subject).to be_an_instance_of(ActiveStorage::Blob) + end end - it 'does not raises in_batches' do - expect(subject).not_to raise_error(NoMethodError) + context 'with files (and http calls)' do + let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } + + before do + allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") + end + + it 'returns a blob with valid files' do + VCR.use_cassette('archive/new_file_to_get_200') do + subject + + File.write('tmp.zip', subject.download, mode: 'wb') + File.open('tmp.zip') do |fd| + files = ZipTricks::FileReader.read_zip_structure(io: fd) + structure = [ + "#{service.send(:base_filename)}/", + "#{service.send(:base_filename)}/dossier-#{dossier.id}/", + "#{service.send(:base_filename)}/dossier-#{dossier.id}/pieces_justificatives/", + "#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", + "#{service.send(:base_filename)}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(PiecesJustificativesService.generate_dossier_export(dossier))}" + ] + expect(files.size).to eq(structure.size) + expect(files.map(&:filename)).to match_array(structure) + end + FileUtils.remove_entry_secure('tmp.zip') + end + end end end end From 0dac59bffc6c1ad49a811f0d879cd18434c9a3e2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 21 Apr 2022 18:48:34 +0200 Subject: [PATCH 11/31] refactor(js): use DOMContentLoaded instead of ds:page:update --- app/javascript/new_design/messagerie.js | 4 ++-- app/javascript/new_design/procedure-context.js | 2 +- app/javascript/new_design/support.js | 2 +- app/javascript/shared/franceconnect.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/new_design/messagerie.js b/app/javascript/new_design/messagerie.js index 0a3e01b39..6f1a010da 100644 --- a/app/javascript/new_design/messagerie.js +++ b/app/javascript/new_design/messagerie.js @@ -54,5 +54,5 @@ function saveMessageContent() { } } -addEventListener('ds:page:update', scrollMessagerie); -addEventListener('ds:page:update', saveMessageContent); +addEventListener('DOMContentLoaded', scrollMessagerie); +addEventListener('DOMContentLoaded', saveMessageContent); diff --git a/app/javascript/new_design/procedure-context.js b/app/javascript/new_design/procedure-context.js index 4ec90a6f2..753811c76 100644 --- a/app/javascript/new_design/procedure-context.js +++ b/app/javascript/new_design/procedure-context.js @@ -14,7 +14,7 @@ function expandProcedureDescription() { descBody.classList.remove('read-more-collapsed'); } -addEventListener('ds:page:update', updateReadMoreVisibility); +addEventListener('DOMContentLoaded', updateReadMoreVisibility); addEventListener('resize', updateReadMoreVisibility); delegate('click', '.read-more-button', expandProcedureDescription); diff --git a/app/javascript/new_design/support.js b/app/javascript/new_design/support.js index 8c590ae00..923f8417e 100644 --- a/app/javascript/new_design/support.js +++ b/app/javascript/new_design/support.js @@ -101,7 +101,7 @@ class ButtonExpand { if (document.querySelector('#contact-form')) { window.addEventListener( - 'ds:page:update', + 'DOMContentLoaded', function () { var buttons = document.querySelectorAll( 'button[aria-expanded][aria-controls], button.button-without-hint' diff --git a/app/javascript/shared/franceconnect.js b/app/javascript/shared/franceconnect.js index 748121209..c739002d0 100644 --- a/app/javascript/shared/franceconnect.js +++ b/app/javascript/shared/franceconnect.js @@ -20,7 +20,7 @@ function init() { } } -addEventListener('ds:page:update', init); +addEventListener('DOMContentLoaded', init); function toggleElement(event) { event.preventDefault(); From 22aefbaa4ad0bcaec99af70a8887e6b0ac643320 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 21 Apr 2022 18:50:08 +0200 Subject: [PATCH 12/31] refactor(js): remove geo-area event handlers --- app/javascript/new_design/champs/carte.js | 40 ----------------------- app/javascript/packs/application.js | 1 - 2 files changed, 41 deletions(-) delete mode 100644 app/javascript/new_design/champs/carte.js diff --git a/app/javascript/new_design/champs/carte.js b/app/javascript/new_design/champs/carte.js deleted file mode 100644 index dbb569c50..000000000 --- a/app/javascript/new_design/champs/carte.js +++ /dev/null @@ -1,40 +0,0 @@ -import { delegate, fire, debounce } from '@utils'; - -const inputHandlers = new Map(); - -addEventListener('ds:page:update', () => { - const inputs = document.querySelectorAll('.areas input[data-geo-area]'); - - for (const input of inputs) { - input.addEventListener('focus', (event) => { - const id = parseInt(event.target.dataset.geoArea); - fire(document, 'map:feature:focus', { id }); - }); - } -}); - -delegate('click', '.areas a[data-geo-area]', (event) => { - event.preventDefault(); - const id = parseInt(event.target.dataset.geoArea); - fire(document, 'map:feature:focus', { id }); -}); - -delegate('input', '.areas input[data-geo-area]', (event) => { - const id = parseInt(event.target.dataset.geoArea); - - let handler = inputHandlers.get(id); - if (!handler) { - handler = debounce(() => { - const input = document.querySelector(`input[data-geo-area="${id}"]`); - if (input) { - fire(document, 'map:feature:update', { - id, - properties: { description: input.value.trim() } - }); - } - }, 200); - inputHandlers.set(id, handler); - } - - handler(); -}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 79a11009e..921cef1fc 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -30,7 +30,6 @@ import '../new_design/messagerie'; import '../new_design/dossiers/auto-save'; import '../new_design/dossiers/auto-upload'; -import '../new_design/champs/carte'; import '../new_design/champs/linked-drop-down-list'; import '../new_design/champs/repetition'; import '../new_design/champs/drop-down-list'; From b5858089246399a33db430b8ab3137b263cb7d6a Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 22 Apr 2022 09:40:52 +0200 Subject: [PATCH 13/31] refactor(js): add geo-area stimulus controller --- app/javascript/components/MapEditor/hooks.ts | 4 +- .../controllers/geo_area_controller.tsx | 53 +++++++++++++++++++ app/javascript/packs/application.js | 2 + .../shared/champs/carte/_geo_area.html.haml | 8 +-- 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 app/javascript/controllers/geo_area_controller.tsx diff --git a/app/javascript/components/MapEditor/hooks.ts b/app/javascript/components/MapEditor/hooks.ts index bc74f63c3..8c941ac58 100644 --- a/app/javascript/components/MapEditor/hooks.ts +++ b/app/javascript/components/MapEditor/hooks.ts @@ -37,9 +37,7 @@ export function useFeatureCollection( type: 'FeatureCollection', features: callback(features) })); - ajax({ url, type: 'GET' }) - .then(() => fire(document, 'ds:page:update')) - .catch(() => null); + ajax({ url, type: 'GET' }).catch(() => null); }, [url, setFeatureCollection] ); diff --git a/app/javascript/controllers/geo_area_controller.tsx b/app/javascript/controllers/geo_area_controller.tsx new file mode 100644 index 000000000..3c1022895 --- /dev/null +++ b/app/javascript/controllers/geo_area_controller.tsx @@ -0,0 +1,53 @@ +import { Controller } from '@hotwired/stimulus'; +import { debounce } from '@utils'; + +type Detail = Record; + +export class GeoAreaController extends Controller { + static values = { + id: String, + description: String + }; + static targets = ['description']; + + declare readonly idValue: string; + declare readonly descriptionTarget: HTMLInputElement; + + onFocus() { + this.globalDispatch('map:feature:focus', { id: this.idValue }); + } + + onClick(event: MouseEvent) { + event.preventDefault(); + this.globalDispatch('map:feature:focus', { id: this.idValue }); + } + + onInput() { + this.debounce(this.updateDescription, 200); + } + + private updateDescription(): void { + this.globalDispatch('map:feature:update', { + id: this.idValue, + properties: { description: this.descriptionTarget.value.trim() } + }); + } + + #debounced = new Map<() => void, () => void>(); + private debounce(fn: () => void, interval: number): void { + let debounced = this.#debounced.get(fn); + if (!debounced) { + debounced = debounce(fn.bind(this), interval); + this.#debounced.set(fn, debounced); + } + debounced(); + } + + private globalDispatch(type: string, detail: Detail): void { + this.dispatch(type, { + detail, + prefix: '', + target: document.documentElement + }); + } +} diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 921cef1fc..be0e31f8f 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -19,6 +19,7 @@ import { registerComponents } from '../controllers/react_controller'; import { TurboEventController } from '../controllers/turbo_event_controller'; +import { GeoAreaController } from '../controllers/geo_area_controller'; import '../new_design/dropdown'; import '../new_design/form-validation'; @@ -95,6 +96,7 @@ Turbo.session.drive = false; const Stimulus = Application.start(); Stimulus.register('react', ReactController); Stimulus.register('turbo-event', TurboEventController); +Stimulus.register('geo-area', GeoAreaController); // Expose globals window.DS = window.DS || DS; diff --git a/app/views/shared/champs/carte/_geo_area.html.haml b/app/views/shared/champs/carte/_geo_area.html.haml index 1915fbd4f..7f945f5d8 100644 --- a/app/views/shared/champs/carte/_geo_area.html.haml +++ b/app/views/shared/champs/carte/_geo_area.html.haml @@ -1,10 +1,10 @@ -%li{ class: editing ? 'mb-1' : 'flex column mb-2' } +%li{ class: editing ? 'mb-1' : 'flex column mb-2', data: { controller: 'geo-area', geo_area_id_value: geo_area.id } } - if editing - = link_to '#', data: { geo_area: geo_area.id } do + = link_to '#', data: { action: 'geo-area#onClick' } do = geo_area_label(geo_area) - = text_field_tag :description, geo_area.description, data: { geo_area: geo_area.id }, placeholder: 'Description', class: 'no-margin' + = text_field_tag :description, geo_area.description, data: { action: 'focus->geo-area#onFocus input->geo-area#onInput', geo_area_target: 'description' }, placeholder: 'Description', class: 'no-margin' - else - = link_to '#', data: { geo_area: geo_area.id } do + = link_to '#', data: { action: 'geo-area#onClick' } do = geo_area_label(geo_area) - if geo_area.description.present? %span From 62dca1c7b0e95f9faf152441b18545a9ff8198ee Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 21 Apr 2022 18:51:03 +0200 Subject: [PATCH 14/31] refactor(js): remove ds:page:update event --- .../controllers/application_controller.ts | 25 ++++++++++++++++ .../controllers/geo_area_controller.tsx | 30 +++---------------- .../controllers/turbo_event_controller.ts | 13 ++------ app/javascript/packs/application.js | 1 - app/javascript/shared/page-update-event.js | 9 ------ 5 files changed, 31 insertions(+), 47 deletions(-) create mode 100644 app/javascript/controllers/application_controller.ts delete mode 100644 app/javascript/shared/page-update-event.js diff --git a/app/javascript/controllers/application_controller.ts b/app/javascript/controllers/application_controller.ts new file mode 100644 index 000000000..3ef5e014f --- /dev/null +++ b/app/javascript/controllers/application_controller.ts @@ -0,0 +1,25 @@ +import { Controller } from '@hotwired/stimulus'; +import { debounce } from '@utils'; + +export type Detail = Record; + +export class ApplicationController extends Controller { + #debounced = new Map<() => void, () => void>(); + + protected debounce(fn: () => void, interval: number): void { + let debounced = this.#debounced.get(fn); + if (!debounced) { + debounced = debounce(fn.bind(this), interval); + this.#debounced.set(fn, debounced); + } + debounced(); + } + + protected globalDispatch(type: string, detail: Detail): void { + this.dispatch(type, { + detail, + prefix: '', + target: document.documentElement + }); + } +} diff --git a/app/javascript/controllers/geo_area_controller.tsx b/app/javascript/controllers/geo_area_controller.tsx index 3c1022895..fd73a236e 100644 --- a/app/javascript/controllers/geo_area_controller.tsx +++ b/app/javascript/controllers/geo_area_controller.tsx @@ -1,16 +1,12 @@ -import { Controller } from '@hotwired/stimulus'; -import { debounce } from '@utils'; +import { ApplicationController } from './application_controller'; -type Detail = Record; - -export class GeoAreaController extends Controller { +export class GeoAreaController extends ApplicationController { static values = { - id: String, - description: String + id: Number }; static targets = ['description']; - declare readonly idValue: string; + declare readonly idValue: number; declare readonly descriptionTarget: HTMLInputElement; onFocus() { @@ -32,22 +28,4 @@ export class GeoAreaController extends Controller { properties: { description: this.descriptionTarget.value.trim() } }); } - - #debounced = new Map<() => void, () => void>(); - private debounce(fn: () => void, interval: number): void { - let debounced = this.#debounced.get(fn); - if (!debounced) { - debounced = debounce(fn.bind(this), interval); - this.#debounced.set(fn, debounced); - } - debounced(); - } - - private globalDispatch(type: string, detail: Detail): void { - this.dispatch(type, { - detail, - prefix: '', - target: document.documentElement - }); - } } diff --git a/app/javascript/controllers/turbo_event_controller.ts b/app/javascript/controllers/turbo_event_controller.ts index 8f959e5a7..f66b63ffd 100644 --- a/app/javascript/controllers/turbo_event_controller.ts +++ b/app/javascript/controllers/turbo_event_controller.ts @@ -1,10 +1,9 @@ -import { Controller } from '@hotwired/stimulus'; import invariant from 'tiny-invariant'; import { z } from 'zod'; -type Detail = Record; +import { ApplicationController, Detail } from './application_controller'; -export class TurboEventController extends Controller { +export class TurboEventController extends ApplicationController { static values = { type: String, detail: Object @@ -17,14 +16,6 @@ export class TurboEventController extends Controller { this.globalDispatch(this.typeValue, this.detailValue); this.element.remove(); } - - private globalDispatch(type: string, detail: Detail): void { - this.dispatch(type, { - detail, - prefix: '', - target: document.documentElement - }); - } } const MutationAction = z.enum(['show', 'hide', 'focus']); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index be0e31f8f..ef4934024 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -5,7 +5,6 @@ import 'whatwg-fetch'; // window.fetch polyfill import { Application } from '@hotwired/stimulus'; import { Turbo } from '@hotwired/turbo-rails'; -import '../shared/page-update-event'; import '../shared/activestorage/ujs'; import '../shared/remote-poller'; import '../shared/safari-11-file-xhr-workaround'; diff --git a/app/javascript/shared/page-update-event.js b/app/javascript/shared/page-update-event.js deleted file mode 100644 index 7a2c2ace2..000000000 --- a/app/javascript/shared/page-update-event.js +++ /dev/null @@ -1,9 +0,0 @@ -import { fire } from '@utils'; - -addEventListener('DOMContentLoaded', function () { - fire(document, 'ds:page:update'); -}); - -addEventListener('ajax:success', function () { - fire(document, 'ds:page:update'); -}); From 23b60c87e928c8c9b33c760ba4663f458ef56bf0 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Thu, 21 Apr 2022 17:39:03 +0200 Subject: [PATCH 15/31] fix(procedure): cloned procedure should not have auto_archive_on fix #7149 --- app/models/procedure.rb | 1 + spec/models/procedure_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 49ffade30..4c6f8f396 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -464,6 +464,7 @@ class Procedure < ApplicationRecord procedure.closed_at = nil procedure.unpublished_at = nil procedure.published_at = nil + procedure.auto_archive_on = nil procedure.lien_notice = nil procedure.published_revision = nil procedure.draft_revision.procedure = procedure diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index d2a3f4ecf..48d87d666 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -603,12 +603,13 @@ describe Procedure do end describe 'procedure status is reset' do - let(:procedure) { create(:procedure, :closed, received_mail: received_mail, service: service) } + let(:procedure) { create(:procedure, :closed, received_mail: received_mail, service: service, auto_archive_on: 3.weeks.from_now) } it 'Not published nor closed' do expect(subject.closed_at).to be_nil expect(subject.published_at).to be_nil expect(subject.unpublished_at).to be_nil + expect(subject.auto_archive_on).to be_nil expect(subject.aasm_state).to eq "brouillon" expect(subject.path).not_to be_nil end From bca97268a8fef3b96f2472d625028e89c4796bc2 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Sun, 24 Apr 2022 11:47:17 +0200 Subject: [PATCH 16/31] fix(upload): errorFromDirectUploadMessage should take Error or string --- app/javascript/shared/activestorage/file-upload-error.ts | 4 +++- app/javascript/shared/activestorage/uploader.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/shared/activestorage/file-upload-error.ts b/app/javascript/shared/activestorage/file-upload-error.ts index 7265ee67e..cd7aeb292 100644 --- a/app/javascript/shared/activestorage/file-upload-error.ts +++ b/app/javascript/shared/activestorage/file-upload-error.ts @@ -63,7 +63,9 @@ export default class FileUploadError extends Error { // 2. Create each kind of error on a different line // (so that Sentry knows they are different kind of errors, from // the line they were created.) -export function errorFromDirectUploadMessage(message: string) { +export function errorFromDirectUploadMessage(messageOrError: string | Error) { + const message = + typeof messageOrError == 'string' ? messageOrError : messageOrError.message; const matches = message.match(/ Status: ([0-9]{1,3})/); const status = matches ? parseInt(matches[1], 10) : undefined; diff --git a/app/javascript/shared/activestorage/uploader.ts b/app/javascript/shared/activestorage/uploader.ts index abbe6ac80..8fd789ab8 100644 --- a/app/javascript/shared/activestorage/uploader.ts +++ b/app/javascript/shared/activestorage/uploader.ts @@ -60,7 +60,7 @@ export default class Uploader { return new Promise((resolve, reject) => { this.directUpload.create((errorMsg, attributes) => { if (errorMsg) { - const error = errorFromDirectUploadMessage(errorMsg.message); + const error = errorFromDirectUploadMessage(errorMsg); reject(error); } else { resolve(attributes.signed_id); From fe6f211cc98f60b8f068b8d9151f30e7f2e3bc1c Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Mon, 25 Apr 2022 12:12:23 +0200 Subject: [PATCH 17/31] ignore instructeur_id in dol --- app/models/dossier_operation_log.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/dossier_operation_log.rb b/app/models/dossier_operation_log.rb index 9a348680f..709ae4e2c 100644 --- a/app/models/dossier_operation_log.rb +++ b/app/models/dossier_operation_log.rb @@ -15,6 +15,8 @@ # instructeur_id :bigint # class DossierOperationLog < ApplicationRecord + self.ignored_columns = [:instructeur_id] + enum operation: { changer_groupe_instructeur: 'changer_groupe_instructeur', passer_en_instruction: 'passer_en_instruction', From 17bf19e3f04d4b8896db97622f31695db848470b Mon Sep 17 00:00:00 2001 From: Remi-Frk <89904222+Remi-Frk@users.noreply.github.com> Date: Fri, 22 Apr 2022 16:16:45 +0200 Subject: [PATCH 18/31] fix(migrate): lien administrateurs_procedures - administrateurs --- ...administrateur_foreign_key_to_administrateurs_procedure.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20220301160753_add_administrateur_foreign_key_to_administrateurs_procedure.rb b/db/migrate/20220301160753_add_administrateur_foreign_key_to_administrateurs_procedure.rb index 944940df1..ec60fcbad 100644 --- a/db/migrate/20220301160753_add_administrateur_foreign_key_to_administrateurs_procedure.rb +++ b/db/migrate/20220301160753_add_administrateur_foreign_key_to_administrateurs_procedure.rb @@ -2,8 +2,8 @@ class AddAdministrateurForeignKeyToAdministrateursProcedure < ActiveRecord::Migr include Database::MigrationHelpers def up - delete_orphans :administrateurs_procedures, :administrateurs_procedures - add_foreign_key :administrateurs_procedures, :administrateurs_procedures + delete_orphans :administrateurs_procedures, :administrateurs + add_foreign_key :administrateurs_procedures, :administrateurs end def down From be43696650061009737caa15de9e5de8ae032472 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 22 Apr 2022 16:05:51 +0200 Subject: [PATCH 19/31] feat(users/procedure/*): add link to texte_juridique or deliberation --- app/views/users/_procedure_footer.html.haml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml index 65c5f2f30..c1cdb13cb 100644 --- a/app/views/users/_procedure_footer.html.haml +++ b/app/views/users/_procedure_footer.html.haml @@ -45,6 +45,15 @@ %ul - politiques.each do |politique| %li= politique + %p.mt-2.footer-header Cadre juridique : + %ul + - if procedure.deliberation.attached? + %li + = link_to url_for(procedure.deliberation), target: '_blank', rel: 'noopener' do + = "Texte cadrant la demande d'information" + - else + %li + = link_to "Texte juridique la demande d'information", procedure.cadre_juridique, target: '_blank', rel: 'noopener' = render partial: 'users/general_footer_row', locals: { dossier: dossier } From 89d192bcfe939f38f9be209f34c2dd64ce1f72ed Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 22 Apr 2022 15:49:34 +0200 Subject: [PATCH 20/31] feat(adminstrateurs#dossier_vide): add button to /dossier_vide so administrateur can have a pdf version of their form to send it in regards to RGPD --- app/views/administrateurs/procedures/show.html.haml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/administrateurs/procedures/show.html.haml b/app/views/administrateurs/procedures/show.html.haml index aac9ca636..a52761878 100644 --- a/app/views/administrateurs/procedures/show.html.haml +++ b/app/views/administrateurs/procedures/show.html.haml @@ -4,6 +4,10 @@ metadatas: ["Créée le #{@procedure.created_at.strftime('%d/%m/%Y')} - n° #{@procedure.id}", "#{@procedure.close? ? "Close le #{@procedure.closed_at.strftime('%d/%m/%Y')}" : @procedure.locked? ? "Publiée - #{procedure_lien(@procedure)}" : "Brouillon"}"] } .container.procedure-admin-container + = link_to @procedure.active_revision.draft? ? commencer_dossier_vide_test_path(path: @procedure.path) : commencer_dossier_vide_path(path: @procedure.path), target: "_blank", rel: "noopener", class: 'button', id: "pdf-procedure" do + %span.icon.printer + PDF + = link_to apercu_admin_procedure_path(@procedure), target: "_blank", rel: "noopener", class: 'button', id: "preview-procedure" do %span.icon.preview Prévisualiser From 418d4ede17e24a2765bf1e4a248d8ac71f1e8c8c Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 25 Apr 2022 14:57:15 +0200 Subject: [PATCH 21/31] test(commencer): test dossier_vide_pdf and dossier_vide_pdf_test --- config/routes.rb | 2 +- .../users/commencer_controller_spec.rb | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 4dcfbe044..4dc41112c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -246,7 +246,7 @@ Rails.application.routes.draw do end namespace :commencer do - get '/test/:path/dossier_vide', action: 'dossier_vide_pdf_test', as: :dossier_vide_test + get '/test/:path/dossier_vide', action: :dossier_vide_pdf_test, as: :dossier_vide_test get '/test/:path', action: 'commencer_test', as: :test get '/:path', action: 'commencer' get '/:path/dossier_vide', action: 'dossier_vide_pdf', as: :dossier_vide diff --git a/spec/controllers/users/commencer_controller_spec.rb b/spec/controllers/users/commencer_controller_spec.rb index 39730b912..1f42717cc 100644 --- a/spec/controllers/users/commencer_controller_spec.rb +++ b/spec/controllers/users/commencer_controller_spec.rb @@ -160,4 +160,41 @@ describe Users::CommencerController, type: :controller do end end end + + describe '#dossier_vide_pdf' do + before { get :dossier_vide_pdf, params: { path: procedure.path } } + + context 'published procedure' do + let(:procedure) { create(:procedure, :published, :with_service, :with_path) } + + it 'works' do + expect(response).to have_http_status(:success) + end + end + context 'not published procedure' do + let(:procedure) { create(:procedure, :with_service, :with_path) } + + it 'redirects to procedure not found' do + expect(response).to have_http_status(302) + end + end + end + + describe '#dossier_vide_test_pdf' do + before { get :dossier_vide_pdf_test, params: { path: procedure.path } } + + context 'not published procedure' do + let(:procedure) { create(:procedure, :with_service, :with_path) } + + it 'works' do + expect(response).to have_http_status(:success) + end + end + context 'published procedure' do + let(:procedure) { create(:procedure, :published, :with_service, :with_path) } + it 'redirect to procedure not found' do + expect(response).to have_http_status(302) + end + end + end end From b1ab0c6ed296fcc67c2e6541c7b8a390721791fa Mon Sep 17 00:00:00 2001 From: Kara Diaby Date: Mon, 25 Apr 2022 15:55:12 +0200 Subject: [PATCH 22/31] =?UTF-8?q?Ajoute=20un=20avertissement=20sur=20les?= =?UTF-8?q?=20champs=20de=20type=20Titre=20Identit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TypesDeChampEditor/components/TypeDeChamp.tsx | 1 + .../components/TypeDeChampPieceJustificative.tsx | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.tsx b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.tsx index 4fd8c31c7..4d5a2b34a 100644 --- a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.tsx +++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.tsx @@ -161,6 +161,7 @@ export const TypeDeChampComponent = SortableElement( /> ; @@ -32,6 +34,17 @@ export function TypeDeChampPieceJustificative({ ); } + + if (isTitreIdentite) { + return ( +
+

+ Dans le cadre de la RGPD, le titre d'identité sera supprimé lors + de l'acceptation du dossier +

+
+ ); + } return null; } From 02f977fd8ddd2e434a0e6435205f9fd8193ad929 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 15 Apr 2022 13:05:46 +0200 Subject: [PATCH 23/31] refactor(champs): refactor champs components to use typescript --- app/javascript/components/Chartkick.jsx | 35 ----------------- app/javascript/components/Chartkick.tsx | 38 +++++++++++++++++++ ...h.jsx => ComboAnnuaireEducationSearch.tsx} | 25 +++++++++--- ...unesSearch.jsx => ComboCommunesSearch.tsx} | 29 +++++++------- ...Search.jsx => ComboDepartementsSearch.tsx} | 28 ++++++++------ .../components/ComboMultipleDropdownList.jsx | 15 -------- .../components/ComboMultipleDropdownList.tsx | 11 ++++++ ...omboPaysSearch.jsx => ComboPaysSearch.tsx} | 10 ++--- ...gionsSearch.jsx => ComboRegionsSearch.tsx} | 10 ++--- app/javascript/components/ComboSearch.tsx | 4 +- app/javascript/shared/utils.ts | 11 ++++-- app/javascript/types.d.ts | 1 + babel.config.js | 6 --- package.json | 2 - yarn.lock | 5 --- 15 files changed, 119 insertions(+), 111 deletions(-) delete mode 100644 app/javascript/components/Chartkick.jsx create mode 100644 app/javascript/components/Chartkick.tsx rename app/javascript/components/{ComboAnnuaireEducationSearch.jsx => ComboAnnuaireEducationSearch.tsx} (50%) rename app/javascript/components/{ComboCommunesSearch.jsx => ComboCommunesSearch.tsx} (85%) rename app/javascript/components/{ComboDepartementsSearch.jsx => ComboDepartementsSearch.tsx} (61%) delete mode 100644 app/javascript/components/ComboMultipleDropdownList.jsx create mode 100644 app/javascript/components/ComboMultipleDropdownList.tsx rename app/javascript/components/{ComboPaysSearch.jsx => ComboPaysSearch.tsx} (68%) rename app/javascript/components/{ComboRegionsSearch.jsx => ComboRegionsSearch.tsx} (69%) diff --git a/app/javascript/components/Chartkick.jsx b/app/javascript/components/Chartkick.jsx deleted file mode 100644 index aef586b53..000000000 --- a/app/javascript/components/Chartkick.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import Chartkick from 'chartkick'; -import Highcharts from 'highcharts'; -import { toggle, delegate } from '@utils'; - -export default function () { - return null; -} - -function toggleChart(event) { - const nextSelectorItem = event.target, - chartClass = event.target.dataset.toggleChart, - nextChart = document.querySelector(chartClass), - nextChartId = nextChart.children[0].id, - currentSelectorItem = nextSelectorItem.parentElement.querySelector( - '.segmented-control-item-active' - ), - currentChart = nextSelectorItem.parentElement.parentElement.querySelector( - '.chart:not(.hidden)' - ); - - // Change the current selector and the next selector states - currentSelectorItem.classList.toggle('segmented-control-item-active'); - nextSelectorItem.classList.toggle('segmented-control-item-active'); - - // Hide the currently shown chart and show the new one - toggle(currentChart); - toggle(nextChart); - - // Reflow needed, see https://github.com/highcharts/highcharts/issues/1979 - Chartkick.charts[nextChartId].getChartObject().reflow(); -} - -delegate('click', '[data-toggle-chart]', toggleChart); - -Chartkick.use(Highcharts); diff --git a/app/javascript/components/Chartkick.tsx b/app/javascript/components/Chartkick.tsx new file mode 100644 index 000000000..e274930d9 --- /dev/null +++ b/app/javascript/components/Chartkick.tsx @@ -0,0 +1,38 @@ +import Chartkick from 'chartkick'; +import Highcharts from 'highcharts'; +import { toggle, delegate } from '@utils'; + +export default function () { + return null; +} + +function toggleChart(event: MouseEvent) { + const nextSelectorItem = event.target as HTMLButtonElement, + chartClass = nextSelectorItem.dataset.toggleChart, + nextChart = chartClass + ? document.querySelector(chartClass) + : undefined, + nextChartId = nextChart?.children[0]?.id, + currentSelectorItem = nextSelectorItem.parentElement?.querySelector( + '.segmented-control-item-active' + ), + currentChart = + nextSelectorItem.parentElement?.parentElement?.querySelector( + '.chart:not(.hidden)' + ); + + // Change the current selector and the next selector states + currentSelectorItem?.classList.toggle('segmented-control-item-active'); + nextSelectorItem.classList.toggle('segmented-control-item-active'); + + // Hide the currently shown chart and show the new one + currentChart && toggle(currentChart); + nextChart && toggle(nextChart); + + // Reflow needed, see https://github.com/highcharts/highcharts/issues/1979 + nextChartId && Chartkick.charts[nextChartId]?.getChartObject()?.reflow(); +} + +delegate('click', '[data-toggle-chart]', toggleChart); + +Chartkick.use(Highcharts); diff --git a/app/javascript/components/ComboAnnuaireEducationSearch.jsx b/app/javascript/components/ComboAnnuaireEducationSearch.tsx similarity index 50% rename from app/javascript/components/ComboAnnuaireEducationSearch.jsx rename to app/javascript/components/ComboAnnuaireEducationSearch.tsx index a0bc4c969..23fc46ec4 100644 --- a/app/javascript/components/ComboAnnuaireEducationSearch.jsx +++ b/app/javascript/components/ComboAnnuaireEducationSearch.tsx @@ -1,16 +1,32 @@ import React from 'react'; import { QueryClientProvider } from 'react-query'; -import ComboSearch from './ComboSearch'; +import ComboSearch, { ComboSearchProps } from './ComboSearch'; import { queryClient } from './shared/queryClient'; -function ComboAnnuaireEducationSearch(props) { +type AnnuaireEducationResult = { + fields: { + identifiant_de_l_etablissement: string; + nom_etablissement: string; + nom_commune: string; + }; +}; + +function transformResults(_: unknown, result: unknown) { + const results = result as { records: AnnuaireEducationResult[] }; + return results.records as AnnuaireEducationResult[]; +} + +export default function ComboAnnuaireEducationSearch( + props: ComboSearchProps +) { return ( records} + transformResults={transformResults} transformResult={({ fields: { identifiant_de_l_etablissement: id, @@ -18,10 +34,7 @@ function ComboAnnuaireEducationSearch(props) { nom_commune } }) => [id, `${nom_etablissement}, ${nom_commune} (${id})`]} - {...props} /> ); } - -export default ComboAnnuaireEducationSearch; diff --git a/app/javascript/components/ComboCommunesSearch.jsx b/app/javascript/components/ComboCommunesSearch.tsx similarity index 85% rename from app/javascript/components/ComboCommunesSearch.jsx rename to app/javascript/components/ComboCommunesSearch.tsx index 85d1b54c3..b8e723ee2 100644 --- a/app/javascript/components/ComboCommunesSearch.jsx +++ b/app/javascript/components/ComboCommunesSearch.tsx @@ -1,19 +1,21 @@ import React from 'react'; import { QueryClientProvider } from 'react-query'; import { matchSorter } from 'match-sorter'; -import PropTypes from 'prop-types'; -import ComboSearch from './ComboSearch'; +import ComboSearch, { ComboSearchProps } from './ComboSearch'; import { queryClient } from './shared/queryClient'; import { ComboDepartementsSearch } from './ComboDepartementsSearch'; import { useHiddenField, groupId } from './shared/hooks'; +type CommuneResult = { code: string; nom: string; codesPostaux: string[] }; + // Avoid hiding similar matches for precise queries (like "Sainte Marie") -function searchResultsLimit(term) { +function searchResultsLimit(term: string) { return term.length > 5 ? 10 : 5; } -function expandResultsWithMultiplePostalCodes(term, results) { +function expandResultsWithMultiplePostalCodes(term: string, result: unknown) { + const results = result as CommuneResult[]; // A single result may have several associated postal codes. // To make the search results more precise, we want to generate // an actual result for each postal code. @@ -44,13 +46,16 @@ const placeholderDepartements = [ ['77 – Seine-et-Marne', 'Melun'], ['22 – Côtes d’Armor', 'Saint-Brieuc'], ['47 – Lot-et-Garonne', 'Agen'] -]; +] as const; const [placeholderDepartement, placeholderCommune] = placeholderDepartements[ Math.floor(Math.random() * (placeholderDepartements.length - 1)) ]; -function ComboCommunesSearch({ id, ...props }) { +export default function ComboCommunesSearch({ + id, + ...props +}: ComboSearchProps & { id: string }) { const group = groupId(id); const [departementValue, setDepartementValue] = useHiddenField( group, @@ -74,14 +79,14 @@ function ComboCommunesSearch({ id, ...props }) { { - setDepartementValue(result?.nom); - setCodeDepartement(result?.code); + setDepartementValue(result?.nom ?? ''); + setCodeDepartement(result?.code ?? ''); }} /> @@ -112,9 +117,3 @@ function ComboCommunesSearch({ id, ...props }) { ); } - -ComboCommunesSearch.propTypes = { - id: PropTypes.string -}; - -export default ComboCommunesSearch; diff --git a/app/javascript/components/ComboDepartementsSearch.jsx b/app/javascript/components/ComboDepartementsSearch.tsx similarity index 61% rename from app/javascript/components/ComboDepartementsSearch.jsx rename to app/javascript/components/ComboDepartementsSearch.tsx index dab35ebb4..6b314b357 100644 --- a/app/javascript/components/ComboDepartementsSearch.jsx +++ b/app/javascript/components/ComboDepartementsSearch.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { QueryClientProvider } from 'react-query'; import { matchSorter } from 'match-sorter'; -import ComboSearch from './ComboSearch'; +import ComboSearch, { ComboSearchProps } from './ComboSearch'; import { queryClient } from './shared/queryClient'; +type DepartementResult = { code: string; nom: string }; + const extraTerms = [{ code: '99', nom: 'Etranger' }]; -function expandResultsWithForeignDepartement(term, results) { +function expandResultsWithForeignDepartement(term: string, result: unknown) { + const results = result as DepartementResult[]; return [ ...results, ...matchSorter(extraTerms, term, { @@ -17,10 +19,17 @@ function expandResultsWithForeignDepartement(term, results) { ]; } +type ComboDepartementsSearchProps = Omit< + ComboSearchProps & { + addForeignDepartement: boolean; + }, + 'transformResult' | 'transformResults' +>; + export function ComboDepartementsSearch({ addForeignDepartement = true, ...props -}) { +}: ComboDepartementsSearchProps) { return ( ); } - -ComboDepartementsSearch.propTypes = { - ...ComboSearch.propTypes, - addForeignDepartement: PropTypes.bool -}; - -export default ComboDepartementsSearchDefault; diff --git a/app/javascript/components/ComboMultipleDropdownList.jsx b/app/javascript/components/ComboMultipleDropdownList.jsx deleted file mode 100644 index b918a5603..000000000 --- a/app/javascript/components/ComboMultipleDropdownList.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { groupId } from './shared/hooks'; -import ComboMultiple from './ComboMultiple'; - -function ComboMultipleDropdownList({ id, ...props }) { - return ; -} - -ComboMultipleDropdownList.propTypes = { - id: PropTypes.string -}; - -export default ComboMultipleDropdownList; diff --git a/app/javascript/components/ComboMultipleDropdownList.tsx b/app/javascript/components/ComboMultipleDropdownList.tsx new file mode 100644 index 000000000..a1cbd188b --- /dev/null +++ b/app/javascript/components/ComboMultipleDropdownList.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import { groupId } from './shared/hooks'; +import ComboMultiple, { ComboMultipleProps } from './ComboMultiple'; + +export default function ComboMultipleDropdownList({ + id, + ...props +}: ComboMultipleProps & { id: string }) { + return ; +} diff --git a/app/javascript/components/ComboPaysSearch.jsx b/app/javascript/components/ComboPaysSearch.tsx similarity index 68% rename from app/javascript/components/ComboPaysSearch.jsx rename to app/javascript/components/ComboPaysSearch.tsx index d44fde47c..c69aaaa4b 100644 --- a/app/javascript/components/ComboPaysSearch.jsx +++ b/app/javascript/components/ComboPaysSearch.tsx @@ -1,20 +1,20 @@ import React from 'react'; import { QueryClientProvider } from 'react-query'; -import ComboSearch from './ComboSearch'; +import ComboSearch, { ComboSearchProps } from './ComboSearch'; import { queryClient } from './shared/queryClient'; -function ComboPaysSearch(props) { +export default function ComboPaysSearch( + props: ComboSearchProps<{ code: string; value: string; label: string }> +) { return ( [code, value, label]} - {...props} /> ); } - -export default ComboPaysSearch; diff --git a/app/javascript/components/ComboRegionsSearch.jsx b/app/javascript/components/ComboRegionsSearch.tsx similarity index 69% rename from app/javascript/components/ComboRegionsSearch.jsx rename to app/javascript/components/ComboRegionsSearch.tsx index e031cc5dd..a1afcc95c 100644 --- a/app/javascript/components/ComboRegionsSearch.jsx +++ b/app/javascript/components/ComboRegionsSearch.tsx @@ -1,20 +1,20 @@ import React from 'react'; import { QueryClientProvider } from 'react-query'; -import ComboSearch from './ComboSearch'; +import ComboSearch, { ComboSearchProps } from './ComboSearch'; import { queryClient } from './shared/queryClient'; -function ComboRegionsSearch(props) { +export default function ComboRegionsSearch( + props: ComboSearchProps<{ code: string; nom: string }> +) { return ( [code, nom]} - {...props} /> ); } - -export default ComboRegionsSearch; diff --git a/app/javascript/components/ComboSearch.tsx b/app/javascript/components/ComboSearch.tsx index f3abd6a94..3ec7f9767 100644 --- a/app/javascript/components/ComboSearch.tsx +++ b/app/javascript/components/ComboSearch.tsx @@ -20,7 +20,7 @@ import { useDeferredSubmit, useHiddenField, groupId } from './shared/hooks'; type TransformResults = (term: string, results: unknown) => Result[]; type TransformResult = ( result: Result -) => [key: string, value: string, label: string]; +) => [key: string, value: string, label?: string]; export type ComboSearchProps = { onChange?: (value: string | null, result?: Result) => void; @@ -28,7 +28,7 @@ export type ComboSearchProps = { scope: string; scopeExtra?: string; minimumInputLength: number; - transformResults: TransformResults; + transformResults?: TransformResults; transformResult: TransformResult; allowInputValues?: boolean; id?: string; diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index 0882f088c..1c0c31ca4 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -42,15 +42,20 @@ export function removeClass(el: HTMLElement, cssClass: string) { el && el.classList.remove(cssClass); } -export function delegate( +export function delegate( eventNames: string, selector: string, - callback: () => void + callback: (event: E) => void ) { eventNames .split(' ') .forEach((eventName) => - Rails.delegate(document, selector, eventName, callback) + Rails.delegate( + document, + selector, + eventName, + callback as (event: Event) => void + ) ); } diff --git a/app/javascript/types.d.ts b/app/javascript/types.d.ts index 200e8886e..097ebf38e 100644 --- a/app/javascript/types.d.ts +++ b/app/javascript/types.d.ts @@ -21,3 +21,4 @@ declare module '@tmcw/togeojson/dist/togeojson.es.js' { } declare module 'react-coordinate-input'; +declare module 'chartkick'; diff --git a/babel.config.js b/babel.config.js index 92513dd6a..d2e2823bb 100644 --- a/babel.config.js +++ b/babel.config.js @@ -86,12 +86,6 @@ module.exports = function (api) { { async: false } - ], - isProductionEnv && [ - 'babel-plugin-transform-react-remove-prop-types', - { - removeImport: true - } ] ].filter(Boolean) }; diff --git a/package.json b/package.json index 7bef976e0..43e7642ac 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "@sentry/browser": "6.12.0", "@tmcw/togeojson": "^4.3.0", "babel-plugin-macros": "^2.8.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "chartkick": "^4.1.1", "core-js": "^3.6.5", "debounce": "^1.2.1", @@ -30,7 +29,6 @@ "is-hotkey": "^0.2.0", "maplibre-gl": "^1.15.2", "match-sorter": "^6.2.0", - "prop-types": "^15.7.2", "react": "^18.0.0", "react-coordinate-input": "^1.0.0", "react-dom": "^18.0.0", diff --git a/yarn.lock b/yarn.lock index adbc387fe..f06ec9512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3417,11 +3417,6 @@ babel-plugin-polyfill-regenerator@^0.3.0: dependencies: "@babel/helper-define-polyfill-provider" "^0.3.0" -babel-plugin-transform-react-remove-prop-types@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" - integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== - backoff@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/backoff/-/backoff-2.5.0.tgz#f616eda9d3e4b66b8ca7fca79f695722c5f8e26f" From 86df16ebda492eb8cd40f169df88f18bb39a919b Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 25 Apr 2022 12:40:16 +0200 Subject: [PATCH 24/31] feat(view_components): add view_components --- Gemfile | 1 + Gemfile.lock | 4 +++ app/components/application_component.rb | 3 +++ .../new_design/dossiers/auto-save.js | 5 ++-- app/views/layouts/component_preview.html.haml | 27 +++++++++++++++++++ config/application.rb | 8 ++++++ spec/rails_helper.rb | 3 +++ 7 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 app/components/application_component.rb create mode 100644 app/views/layouts/component_preview.html.haml diff --git a/Gemfile b/Gemfile index 417367210..592bc32ba 100644 --- a/Gemfile +++ b/Gemfile @@ -86,6 +86,7 @@ gem 'spreadsheet_architect' gem 'strong_migrations' # lint database migrations gem 'turbo-rails' gem 'typhoeus' +gem 'view_component' gem 'warden' gem 'webpacker' gem 'zipline' diff --git a/Gemfile.lock b/Gemfile.lock index b3fb8c66d..0387b05f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -741,6 +741,9 @@ GEM activemodel (>= 3.0.0) public_suffix vcr (6.0.0) + view_component (2.53.0) + activesupport (>= 5.0.0, < 8.0) + method_source (~> 1.0) virtus (2.0.0) axiom-types (~> 0.1) coercible (~> 1.0) @@ -904,6 +907,7 @@ DEPENDENCIES turbo-rails typhoeus vcr + view_component warden web-console webdrivers (~> 4.0) diff --git a/app/components/application_component.rb b/app/components/application_component.rb new file mode 100644 index 000000000..5235b0900 --- /dev/null +++ b/app/components/application_component.rb @@ -0,0 +1,3 @@ +class ApplicationComponent < ViewComponent::Base + include ViewComponent::Translatable +end diff --git a/app/javascript/new_design/dossiers/auto-save.js b/app/javascript/new_design/dossiers/auto-save.js index bc57c9b86..e437cdf7f 100644 --- a/app/javascript/new_design/dossiers/auto-save.js +++ b/app/javascript/new_design/dossiers/auto-save.js @@ -10,8 +10,9 @@ import { removeClass } from '@utils'; -const AUTOSAVE_DEBOUNCE_DELAY = gon.autosave.debounce_delay; -const AUTOSAVE_STATUS_VISIBLE_DURATION = gon.autosave.status_visible_duration; +const AUTOSAVE_DEBOUNCE_DELAY = window?.gon?.autosave?.debounce_delay; +const AUTOSAVE_STATUS_VISIBLE_DURATION = + window?.gon?.autosave?.status_visible_duration; // Create a controller responsible for queuing autosave operations. const autoSaveController = new AutoSaveController(); diff --git a/app/views/layouts/component_preview.html.haml b/app/views/layouts/component_preview.html.haml new file mode 100644 index 000000000..cc9cebe04 --- /dev/null +++ b/app/views/layouts/component_preview.html.haml @@ -0,0 +1,27 @@ +!!! 5 +%html{ lang: html_lang, class: yield(:root_class) } + %head + %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } + %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } + %meta{ name: "viewport", content: "width=device-width, initial-scale=1" } + = csrf_meta_tags + + %title + = content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME + + = favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16") + = favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32") + = favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96") + + = javascript_packs_with_chunks_tag 'application', defer: true + + = preload_link_tag(asset_url("Muli-Regular.woff2")) + = preload_link_tag(asset_url("Muli-Bold.woff2")) + + = stylesheet_link_tag 'application', media: 'all' + + %body{ class: browser.platform.ios? ? 'ios' : nil } + .page-wrapper + %main.m-6 + = content_for?(:content) ? yield(:content) : yield + %turbo-events diff --git a/config/application.rb b/config/application.rb index e24e39038..5fce62b9e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -81,5 +81,13 @@ module TPS # Custom Configuration # @see https://guides.rubyonrails.org/configuring.html#custom-configuration config.x.clamav.enabled = ENV.fetch("CLAMAV_ENABLED", "enabled") == "enabled" + + config.view_component.generate_sidecar = true + config.view_component.generate_locale = true + config.view_component.generate_distinct_locale_files = true + config.view_component.generate_preview = true + config.view_component.show_previews_source = true + config.view_component.default_preview_layout = 'component_preview' + config.view_component.preview_paths << "#{Rails.root}/spec/components/previews" end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index b58e5553d..3528d4433 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,6 +13,7 @@ require 'rspec/rails' require 'axe-rspec' require 'devise' require 'shoulda-matchers' +require 'view_component/test_helpers' # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are @@ -124,4 +125,6 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::IntegrationHelpers, type: :system + config.include ViewComponent::TestHelpers, type: :component + config.include Capybara::RSpecMatchers, type: :component end From 004c9f6e18dce276666f0188d6fad4523bca3798 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 25 Apr 2022 13:55:10 +0200 Subject: [PATCH 25/31] fix(i18n): disable i18n-tasks on view components for now --- config/i18n-tasks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index a4e992c04..702153547 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -68,6 +68,7 @@ search: - app/assets/images - app/assets/fonts - app/assets/videos + - app/components ## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`: ## If specified, this settings takes priority over `exclude`, but `exclude` still applies. From 009c44cc20bb598c9b4363f625702ca4f96d3496 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 09:40:45 +0200 Subject: [PATCH 26/31] refactor(avis): remove unused controller action --- app/controllers/experts/avis_controller.rb | 16 ------- config/routes.rb | 1 - .../messages/message.html.haml_spec.rb | 47 ------------------- 3 files changed, 64 deletions(-) diff --git a/app/controllers/experts/avis_controller.rb b/app/controllers/experts/avis_controller.rb index a298d4485..35ff9c99d 100644 --- a/app/controllers/experts/avis_controller.rb +++ b/app/controllers/experts/avis_controller.rb @@ -113,22 +113,6 @@ module Experts end end - def delete_commentaire - commentaire = avis.dossier.commentaires.find(params[:commentaire]) - if commentaire.sent_by?(current_expert) - commentaire.piece_jointe.purge_later if commentaire.piece_jointe.attached? - commentaire.discard! - commentaire.update!(body: '') - flash[:notice] = t('views.shared.commentaires.destroy.notice') - else - flash[:alert] = I18n.t('views.shared.commentaires.destroy.alert_reasons.acl') - end - redirect_to(messagerie_expert_avis_path(avis.procedure, avis)) - rescue Discard::RecordNotDiscarded - flash[:alert] = I18n.t('views.shared.commentaires.destroy.alert_reasons.already_discarded') - redirect_to(messagerie_expert_avis_path(avis.procedure, avis)) - end - def bilans_bdf if avis.dossier.etablissement&.entreprise_bilans_bdf.present? extension = params[:format] diff --git a/config/routes.rb b/config/routes.rb index 4dc41112c..655a3247e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,7 +310,6 @@ Rails.application.routes.draw do get 'instruction' get 'messagerie' post 'commentaire' => 'avis#create_commentaire' - delete 'delete_commentaire' => 'avis#delete_commentaire' post 'avis' => 'avis#create_avis' get 'bilans_bdf' get 'telecharger_pjs' => 'avis#telecharger_pjs' diff --git a/spec/views/shared/dossiers/messages/message.html.haml_spec.rb b/spec/views/shared/dossiers/messages/message.html.haml_spec.rb index 106b11178..00744a27a 100644 --- a/spec/views/shared/dossiers/messages/message.html.haml_spec.rb +++ b/spec/views/shared/dossiers/messages/message.html.haml_spec.rb @@ -86,51 +86,4 @@ describe 'shared/dossiers/messages/message.html.haml', type: :view do end end end - - context 'with an expert message' do - describe 'delete message button for expert' do - let(:expert) { create(:expert) } - let(:procedure) { create(:procedure) } - let(:dossier) { create(:dossier, :en_construction, commentaires: [commentaire], procedure: procedure) } - let(:experts_procedure) { create(:experts_procedure, procedure: procedure, expert: expert) } - let!(:avis) { create(:avis, email: nil, experts_procedure: experts_procedure) } - subject { render 'shared/dossiers/messages/message.html.haml', commentaire: commentaire, messagerie_seen_at: seen_at, connected_user: expert, show_reply_button: true } - let(:form_url) { delete_commentaire_expert_avis_path(avis.procedure, avis, commentaire: commentaire) } - - before do - assign(:avis, avis) - end - - context 'on a procedure where commentaire had been written by connected expert' do - let(:commentaire) { create(:commentaire, expert: expert, body: 'Second message') } - - it { is_expected.to have_selector("form[action=\"#{form_url}\"]") } - end - - context 'on a procedure where commentaire had been written by connected expert and discarded' do - let(:commentaire) { create(:commentaire, expert: expert, body: 'Second message', discarded_at: 2.days.ago) } - - it { is_expected.not_to have_selector("form[action=\"#{form_url}\"]") } - it { is_expected.not_to have_selector(".rich-text", text: I18n.t(t('views.shared.commentaires.destroy.deleted_body'))) } - end - - context 'on a procedure where commentaire had been written by connected an user' do - let(:commentaire) { create(:commentaire, email: create(:user).email, body: 'Second message') } - - it { is_expected.not_to have_selector("form[action=\"#{form_url}\"]") } - end - - context 'on a procedure where commentaire had been written by connected an instructeur' do - let(:commentaire) { create(:commentaire, instructeur: create(:instructeur), body: 'Second message') } - - it { is_expected.not_to have_selector("form[action=\"#{form_url}\"]") } - end - - context 'on a procedure where commentaire had been written another expert' do - let(:commentaire) { create(:commentaire, expert: create(:expert), body: 'Second message') } - - it { is_expected.not_to have_selector("form[action=\"#{form_url}\"]") } - end - end - end end From 91879b89cefb06677d91665b6535e3fd0be79670 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 19 Apr 2022 09:40:59 +0200 Subject: [PATCH 27/31] fix(commentaire): unified destroy commentaire --- .../instructeurs/commentaires_controller.rb | 36 ++++++++++++------- app/models/commentaire.rb | 6 ++++ .../commentaires/destroy.turbo_stream.haml | 2 ++ .../shared/dossiers/_messagerie.html.haml | 2 +- .../dossiers/messages/_message.html.haml | 9 +++-- config/locales/en.yml | 3 -- config/locales/fr.yml | 3 -- .../views/instructeurs/commentaires/en.yml | 7 ++++ .../views/instructeurs/commentaires/fr.yml | 7 ++++ config/locales/views/instructeurs/en.yml | 4 ++- config/locales/views/instructeurs/fr.yml | 2 ++ config/locales/views/shared/dossiers/en.yml | 9 +++++ config/locales/views/shared/dossiers/fr.yml | 9 +++++ config/locales/views/shared/en.yml | 6 ---- config/locales/views/shared/fr.yml | 6 ---- .../commentaires_controller_spec.rb | 28 +++++++-------- 16 files changed, 88 insertions(+), 51 deletions(-) create mode 100644 app/views/instructeurs/commentaires/destroy.turbo_stream.haml create mode 100644 config/locales/views/instructeurs/commentaires/en.yml create mode 100644 config/locales/views/instructeurs/commentaires/fr.yml create mode 100644 config/locales/views/shared/dossiers/en.yml create mode 100644 config/locales/views/shared/dossiers/fr.yml diff --git a/app/controllers/instructeurs/commentaires_controller.rb b/app/controllers/instructeurs/commentaires_controller.rb index fe36bfc1a..cfe81f0ec 100644 --- a/app/controllers/instructeurs/commentaires_controller.rb +++ b/app/controllers/instructeurs/commentaires_controller.rb @@ -1,21 +1,33 @@ -# frozen_string_literal: true - module Instructeurs class CommentairesController < ProceduresController + after_action :mark_messagerie_as_read + def destroy - commentaire = Dossier.find(params[:dossier_id]).commentaires.find(params[:id]) - if commentaire.sent_by?(current_instructeur) - commentaire.piece_jointe.purge_later if commentaire.piece_jointe.attached? - commentaire.discard! - commentaire.update!(body: '') - flash[:notice] = t('views.shared.commentaires.destroy.notice') + if commentaire.sent_by?(current_instructeur) || commentaire.sent_by?(current_expert) + commentaire.soft_delete! + + flash.notice = t('.notice') else - flash[:alert] = I18n.t('views.shared.commentaires.destroy.alert_reasons.acl') + flash.alert = t('.alert_acl') end - redirect_to(messagerie_instructeur_dossier_path(params[:procedure_id], params[:dossier_id])) rescue Discard::RecordNotDiscarded - flash[:alert] = I18n.t('views.shared.commentaires.destroy.alert_reasons.already_discarded') - redirect_to(messagerie_instructeur_dossier_path(params[:procedure_id], params[:dossier_id])) + # i18n-tasks-use t('instructeurs.commentaires.destroy.alert_already_discarded') + flash.alert = t('.alert_already_discarded') + end + + private + + def mark_messagerie_as_read + if commentaire.sent_by?(current_instructeur) + current_instructeur.mark_tab_as_seen(commentaire.dossier, :messagerie) + end + end + + def commentaire + @commentaire ||= Dossier + .find(params[:dossier_id]) + .commentaires + .find(params[:id]) end end end diff --git a/app/models/commentaire.rb b/app/models/commentaire.rb index 61a48be6d..05103e367 100644 --- a/app/models/commentaire.rb +++ b/app/models/commentaire.rb @@ -89,6 +89,12 @@ class Commentaire < ApplicationRecord end end + def soft_delete! + piece_jointe.purge_later if piece_jointe.attached? + discard! + update! body: '' + end + private def notify diff --git a/app/views/instructeurs/commentaires/destroy.turbo_stream.haml b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml new file mode 100644 index 000000000..fb9a0f1a9 --- /dev/null +++ b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml @@ -0,0 +1,2 @@ +- if @commentaire.discarded? + = turbo_stream.update @commentaire, partial: "shared/dossiers/messages/message", locals: { commentaire: @commentaire, connected_user: @commentaire.instructeur || @commentaire.expert, messagerie_seen_at: nil, show_reply_button: false } diff --git a/app/views/shared/dossiers/_messagerie.html.haml b/app/views/shared/dossiers/_messagerie.html.haml index 039f01043..7e3a33f18 100644 --- a/app/views/shared/dossiers/_messagerie.html.haml +++ b/app/views/shared/dossiers/_messagerie.html.haml @@ -1,7 +1,7 @@ .messagerie.container %ul.messages-list - dossier.commentaires.with_attached_piece_jointe.each do |commentaire| - %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user) } + %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user), id: dom_id(commentaire) } = render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user) } - if dossier.messagerie_available? diff --git a/app/views/shared/dossiers/messages/_message.html.haml b/app/views/shared/dossiers/messages/_message.html.haml index dececc1b6..646692125 100644 --- a/app/views/shared/dossiers/messages/_message.html.haml +++ b/app/views/shared/dossiers/messages/_message.html.haml @@ -5,17 +5,16 @@ %span.mail = render partial: 'shared/dossiers/messages/message_issuer', locals: { commentaire: commentaire, connected_user: connected_user } - if commentaire_is_from_guest(commentaire) - %span.guest= t('views.shared.dossiers.messages.message.guest') + %span.guest= t('.guest') %span.date{ class: highlight_if_unseen_class(messagerie_seen_at, commentaire.created_at) } = commentaire_date(commentaire) .rich-text= pretty_commentaire(commentaire) .message-extras.flex.justify-start - if commentaire.soft_deletable?(connected_user) - - path = connected_user.is_a?(Instructeur) ? instructeur_commentaire_path(commentaire.dossier.procedure, commentaire.dossier, commentaire) : delete_commentaire_expert_avis_path(@avis.procedure, @avis, commentaire: commentaire) - = button_to path, method: :delete, class: 'button danger', data: { confirm: t('views.shared.commentaires.destroy.confirm') } do + = button_to instructeur_commentaire_path(commentaire.dossier.procedure, commentaire.dossier, commentaire), method: :delete, class: 'button danger', form: { data: { turbo: true, turbo_confirm: t('.confirm') } } do %span.icon.delete - = t('views.shared.commentaires.destroy.button') + = t('.delete_button') - if commentaire.piece_jointe.attached? .attachment-link @@ -24,4 +23,4 @@ - if show_reply_button = button_tag type: 'button', class: 'button small message-answer-button', onclick: 'document.querySelector("#commentaire_body").focus()' do %span.icon.reply - = t('views.shared.dossiers.messages.message.reply') + = t('.reply') diff --git a/config/locales/en.yml b/config/locales/en.yml index b12a43ed0..e122e7020 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -130,9 +130,6 @@ en: message_issuer: automatic_email: "Automatic email" you: "You" - message: - reply: "Reply" - guest: "Guest" form: send_message: "Send message" attachment_size: "(attachment size max : 20 Mo)" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index e3574b2ef..2fcff2cae 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -125,9 +125,6 @@ fr: message_issuer: automatic_email: "Email automatique" you: "Vous" - message: - reply: "Répondre" - guest: "Invité" form: send_message: "Envoyer le message" attachment_size: "(taille max : 20 Mo)" diff --git a/config/locales/views/instructeurs/commentaires/en.yml b/config/locales/views/instructeurs/commentaires/en.yml new file mode 100644 index 000000000..6915ce6d4 --- /dev/null +++ b/config/locales/views/instructeurs/commentaires/en.yml @@ -0,0 +1,7 @@ +en: + instructeurs: + commentaires: + destroy: + notice: Your message had been deleted + alert_acl: "Can not destroy message: it does not belong to you" + alert_already_discarded: "Can not destroy message: it was already destroyed" diff --git a/config/locales/views/instructeurs/commentaires/fr.yml b/config/locales/views/instructeurs/commentaires/fr.yml new file mode 100644 index 000000000..fd2b20b78 --- /dev/null +++ b/config/locales/views/instructeurs/commentaires/fr.yml @@ -0,0 +1,7 @@ +fr: + instructeurs: + commentaires: + destroy: + notice: Votre message a été supprimé + alert_acl: Impossible de supprimer le message, celui ci ne vous appartient pas + alert_already_discarded: Ce message a déjà été supprimé diff --git a/config/locales/views/instructeurs/en.yml b/config/locales/views/instructeurs/en.yml index 9ebb913db..ebd1f3cea 100644 --- a/config/locales/views/instructeurs/en.yml +++ b/config/locales/views/instructeurs/en.yml @@ -1,5 +1,7 @@ en: instructeurs: + commentaires_controller: + alert_already_discarded: "Can not destroy message: it was already destroyed" procedure: archive_pending_html: Archive creation pending
(requested %{created_period} ago) - archive_ready_html: Download archive
(requested %{generated_period} ago) \ No newline at end of file + archive_ready_html: Download archive
(requested %{generated_period} ago) diff --git a/config/locales/views/instructeurs/fr.yml b/config/locales/views/instructeurs/fr.yml index 153dbc679..2b5c8c035 100644 --- a/config/locales/views/instructeurs/fr.yml +++ b/config/locales/views/instructeurs/fr.yml @@ -1,5 +1,7 @@ fr: instructeurs: + commentaires_controller: + alert_already_discarded: Ce message a déjà été supprimé procedure: archive_pending_html: Archive en cours de création
(demandée il y a %{created_period}) archive_ready_html: Télécharger l’archive
(demandée il y a %{generated_period}) diff --git a/config/locales/views/shared/dossiers/en.yml b/config/locales/views/shared/dossiers/en.yml new file mode 100644 index 000000000..50fe2ce18 --- /dev/null +++ b/config/locales/views/shared/dossiers/en.yml @@ -0,0 +1,9 @@ +en: + shared: + dossiers: + messages: + message: + reply: Reply + guest: Guest + delete_button: Delete this message + confirm: Are you sure you want to delete this message ? diff --git a/config/locales/views/shared/dossiers/fr.yml b/config/locales/views/shared/dossiers/fr.yml new file mode 100644 index 000000000..fe70e4cf7 --- /dev/null +++ b/config/locales/views/shared/dossiers/fr.yml @@ -0,0 +1,9 @@ +fr: + shared: + dossiers: + messages: + message: + reply: Répondre + guest: Invité + delete_button: Supprimer le message + confirm: Êtes-vous sûr de vouloir supprimer ce message ? diff --git a/config/locales/views/shared/en.yml b/config/locales/views/shared/en.yml index 5770dd40f..bb67c5377 100644 --- a/config/locales/views/shared/en.yml +++ b/config/locales/views/shared/en.yml @@ -22,10 +22,4 @@ en: signin: 'Sign in' commentaires: destroy: - button: 'Destroy this message' - confirm: "Are you sure you want to destroy this message ?" deleted_body: Message deleted - notice: 'Your message had been deleted' - alert_reasons: - acl: "Can not destroy message: it does not belong to you" - already_discarded: "Can not destroy message: it was already destroyed" diff --git a/config/locales/views/shared/fr.yml b/config/locales/views/shared/fr.yml index 8325949e8..42f484605 100644 --- a/config/locales/views/shared/fr.yml +++ b/config/locales/views/shared/fr.yml @@ -22,10 +22,4 @@ fr: signin: 'Connexion' commentaires: destroy: - button: 'Supprimer le message' - confirm: "Êtes-vous sûr de vouloir supprimer ce message ?" deleted_body: Message supprimé - notice: 'Votre message a été supprimé' - alert_reasons: - acl: "Impossible de supprimer le message, celui ci ne vous appartient pas" - already_discarded: "Ce message a déjà été supprimé" diff --git a/spec/controllers/instructeurs/commentaires_controller_spec.rb b/spec/controllers/instructeurs/commentaires_controller_spec.rb index b21ade81c..465adf50d 100644 --- a/spec/controllers/instructeurs/commentaires_controller_spec.rb +++ b/spec/controllers/instructeurs/commentaires_controller_spec.rb @@ -4,33 +4,33 @@ describe Instructeurs::CommentairesController, type: :controller do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:procedure, :published, :for_individual, instructeurs: [instructeur]) } let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure) } + render_views before { sign_in(instructeur.user) } describe 'destroy' do + render_views + context 'when it works' do let(:commentaire) { create(:commentaire, instructeur: instructeur, dossier: dossier) } - subject { delete :destroy, params: { dossier_id: dossier.id, procedure_id: procedure.id, id: commentaire.id } } + subject { delete :destroy, params: { dossier_id: dossier.id, procedure_id: procedure.id, id: commentaire.id }, format: :turbo_stream } - it 'redirect to dossier' do - expect(subject).to redirect_to(messagerie_instructeur_dossier_path(dossier.procedure, dossier)) - end - it 'flash success' do - subject - expect(flash[:notice]).to eq(I18n.t('views.shared.commentaires.destroy.notice')) + it 'respond with OK and flash' do + expect(subject).to have_http_status(:ok) + expect(subject.body).to include('Message supprimé') + expect(subject.body).to include('alert-success') + expect(subject.body).to include('Votre message a été supprimé') end end context 'when dossier had been discarded' do let(:commentaire) { create(:commentaire, instructeur: instructeur, dossier: dossier, discarded_at: 2.hours.ago) } - subject { delete :destroy, params: { dossier_id: dossier.id, procedure_id: procedure.id, id: commentaire.id } } + subject { delete :destroy, params: { dossier_id: dossier.id, procedure_id: procedure.id, id: commentaire.id }, format: :turbo_stream } - it 'redirect to dossier' do - expect(subject).to redirect_to(messagerie_instructeur_dossier_path(dossier.procedure, dossier)) - end - it 'flash success' do - subject - expect(flash[:alert]).to eq(I18n.t('views.shared.commentaires.destroy.alert_reasons.already_discarded')) + it 'respond with OK and flash' do + expect(subject).to have_http_status(:ok) + expect(subject.body).to include('alert-danger') + expect(subject.body).to include('Ce message a déjà été supprimé') end end end From d2ab8b559330010ff816283a2b723e8f9958e9e6 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Mon, 25 Apr 2022 12:41:01 +0200 Subject: [PATCH 28/31] feat(message): replace message partial with MessageComponent --- app/components/dossiers/message_component.rb | 58 +++++++++++++++ .../message_component.en.yml | 9 +++ .../message_component.fr.yml | 9 +++ .../message_component.html.haml} | 14 ++-- app/helpers/commentaire_helper.rb | 16 ---- .../commentaires/destroy.turbo_stream.haml | 3 +- .../instructeurs/dossiers/print.html.haml | 2 +- .../shared/dossiers/_messagerie.html.haml | 2 +- .../dossiers/messages/_message_icon.html.haml | 7 -- .../messages/_message_issuer.html.haml | 6 -- .../dossiers/show/_latest_message.html.haml | 2 +- config/locales/en.yml | 3 - config/locales/fr.yml | 3 - config/locales/views/instructeurs/en.yml | 2 - config/locales/views/instructeurs/fr.yml | 2 - config/locales/views/shared/dossiers/en.yml | 9 --- config/locales/views/shared/dossiers/fr.yml | 9 --- config/locales/views/shared/en.yml | 3 - config/locales/views/shared/fr.yml | 3 - .../dossiers/message_component_spec.rb} | 73 ++++++++++++++++--- .../dossiers/message_component_preview.rb | 31 ++++++++ spec/helpers/commentaire_helper_spec.rb | 47 ------------ 22 files changed, 182 insertions(+), 131 deletions(-) create mode 100644 app/components/dossiers/message_component.rb create mode 100644 app/components/dossiers/message_component/message_component.en.yml create mode 100644 app/components/dossiers/message_component/message_component.fr.yml rename app/{views/shared/dossiers/messages/_message.html.haml => components/dossiers/message_component/message_component.html.haml} (60%) delete mode 100644 app/views/shared/dossiers/messages/_message_icon.html.haml delete mode 100644 app/views/shared/dossiers/messages/_message_issuer.html.haml delete mode 100644 config/locales/views/shared/dossiers/en.yml delete mode 100644 config/locales/views/shared/dossiers/fr.yml rename spec/{views/shared/dossiers/messages/message.html.haml_spec.rb => components/dossiers/message_component_spec.rb} (59%) create mode 100644 spec/components/previews/dossiers/message_component_preview.rb diff --git a/app/components/dossiers/message_component.rb b/app/components/dossiers/message_component.rb new file mode 100644 index 000000000..8e2a185a1 --- /dev/null +++ b/app/components/dossiers/message_component.rb @@ -0,0 +1,58 @@ +class Dossiers::MessageComponent < ApplicationComponent + def initialize(commentaire:, connected_user:, messagerie_seen_at: nil, show_reply_button: false) + @commentaire = commentaire + @connected_user = connected_user + @messagerie_seen_at = messagerie_seen_at + @show_reply_button = show_reply_button + end + + attr_reader :commentaire, :connected_user, :messagerie_seen_at + + private + + def show_reply_button? + @show_reply_button + end + + def highlight_if_unseen_class + helpers.highlight_if_unseen_class(@messagerie_seen_at, commentaire.created_at) + end + + def icon_path + if commentaire.sent_by_system? + 'icons/mail.svg' + elsif commentaire.sent_by?(connected_user) + 'icons/account-circle.svg' + else + 'icons/blue-person.svg' + end + end + + def commentaire_issuer + if commentaire.sent_by_system? + t('.automatic_email') + elsif commentaire.sent_by?(connected_user) + t('.you') + else + commentaire.redacted_email + end + end + + def commentaire_from_guest? + commentaire.dossier.invites.map(&:email).include?(commentaire.email) + end + + def commentaire_date + is_current_year = (commentaire.created_at.year == Time.zone.today.year) + l(commentaire.created_at, format: is_current_year ? :message_date : :message_date_with_year) + end + + def commentaire_body + if commentaire.discarded? + t('.deleted_body') + else + body_formatted = commentaire.sent_by_system? ? commentaire.body : simple_format(commentaire.body) + sanitize(body_formatted) + end + end +end diff --git a/app/components/dossiers/message_component/message_component.en.yml b/app/components/dossiers/message_component/message_component.en.yml new file mode 100644 index 000000000..a24af5c49 --- /dev/null +++ b/app/components/dossiers/message_component/message_component.en.yml @@ -0,0 +1,9 @@ +--- +en: + reply: Reply + guest: Guest + delete_button: Delete this message + confirm: Are you sure you want to delete this message ? + automatic_email: Automatic email + you: You + deleted_body: Message deleted diff --git a/app/components/dossiers/message_component/message_component.fr.yml b/app/components/dossiers/message_component/message_component.fr.yml new file mode 100644 index 000000000..4386b2ea2 --- /dev/null +++ b/app/components/dossiers/message_component/message_component.fr.yml @@ -0,0 +1,9 @@ +--- +fr: + reply: Répondre + guest: Invité + delete_button: Supprimer le message + confirm: Êtes-vous sûr de vouloir supprimer ce message ? + automatic_email: Email automatique + you: Vous + deleted_body: Message supprimé diff --git a/app/views/shared/dossiers/messages/_message.html.haml b/app/components/dossiers/message_component/message_component.html.haml similarity index 60% rename from app/views/shared/dossiers/messages/_message.html.haml rename to app/components/dossiers/message_component/message_component.html.haml index 646692125..c19f78791 100644 --- a/app/views/shared/dossiers/messages/_message.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -1,14 +1,14 @@ -= render partial: 'shared/dossiers/messages/message_icon', locals: { commentaire: commentaire, connected_user: connected_user } += image_tag(icon_path, class: 'person-icon', alt: '') .width-100 %h2 %span.mail - = render partial: 'shared/dossiers/messages/message_issuer', locals: { commentaire: commentaire, connected_user: connected_user } - - if commentaire_is_from_guest(commentaire) + = commentaire_issuer + - if commentaire_from_guest? %span.guest= t('.guest') - %span.date{ class: highlight_if_unseen_class(messagerie_seen_at, commentaire.created_at) } - = commentaire_date(commentaire) - .rich-text= pretty_commentaire(commentaire) + %span.date{ class: highlight_if_unseen_class } + = commentaire_date + .rich-text= commentaire_body .message-extras.flex.justify-start - if commentaire.soft_deletable?(connected_user) @@ -20,7 +20,7 @@ .attachment-link = render partial: "shared/attachment/show", locals: { attachment: commentaire.piece_jointe.attachment } - - if show_reply_button + - if show_reply_button? = button_tag type: 'button', class: 'button small message-answer-button', onclick: 'document.querySelector("#commentaire_body").focus()' do %span.icon.reply = t('.reply') diff --git a/app/helpers/commentaire_helper.rb b/app/helpers/commentaire_helper.rb index 2518c8506..8ba5bfab8 100644 --- a/app/helpers/commentaire_helper.rb +++ b/app/helpers/commentaire_helper.rb @@ -12,20 +12,4 @@ module CommentaireHelper I18n.t('helpers.commentaire.reply_in_mailbox') end end - - def commentaire_is_from_guest(commentaire) - commentaire.dossier.invites.map(&:email).include?(commentaire.email) - end - - def commentaire_date(commentaire) - is_current_year = (commentaire.created_at.year == Time.zone.today.year) - template = is_current_year ? :message_date : :message_date_with_year - I18n.l(commentaire.created_at, format: template) - end - - def pretty_commentaire(commentaire) - return t('views.shared.commentaires.destroy.deleted_body') if commentaire.discarded? - body_formatted = commentaire.sent_by_system? ? commentaire.body : simple_format(commentaire.body) - sanitize(body_formatted) - end end diff --git a/app/views/instructeurs/commentaires/destroy.turbo_stream.haml b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml index fb9a0f1a9..d84c39d0d 100644 --- a/app/views/instructeurs/commentaires/destroy.turbo_stream.haml +++ b/app/views/instructeurs/commentaires/destroy.turbo_stream.haml @@ -1,2 +1,3 @@ - if @commentaire.discarded? - = turbo_stream.update @commentaire, partial: "shared/dossiers/messages/message", locals: { commentaire: @commentaire, connected_user: @commentaire.instructeur || @commentaire.expert, messagerie_seen_at: nil, show_reply_button: false } + = turbo_stream.update @commentaire do + = render Dossiers::MessageComponent.new(commentaire: @commentaire, connected_user: @commentaire.instructeur || @commentaire.expert) diff --git a/app/views/instructeurs/dossiers/print.html.haml b/app/views/instructeurs/dossiers/print.html.haml index 656085d58..12c2fa5d0 100644 --- a/app/views/instructeurs/dossiers/print.html.haml +++ b/app/views/instructeurs/dossiers/print.html.haml @@ -49,7 +49,7 @@ %ul.messages-list - @dossier.commentaires.with_attached_piece_jointe.each do |commentaire| %li - = render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: current_instructeur, messagerie_seen_at: nil, show_reply_button: false } + = render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: current_instructeur) %script{ type: "text/javascript" } window.print(); diff --git a/app/views/shared/dossiers/_messagerie.html.haml b/app/views/shared/dossiers/_messagerie.html.haml index 7e3a33f18..e72046db4 100644 --- a/app/views/shared/dossiers/_messagerie.html.haml +++ b/app/views/shared/dossiers/_messagerie.html.haml @@ -2,7 +2,7 @@ %ul.messages-list - dossier.commentaires.with_attached_piece_jointe.each do |commentaire| %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user), id: dom_id(commentaire) } - = render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user) } + = render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user)) - if dossier.messagerie_available? = render partial: "shared/dossiers/messages/form", locals: { commentaire: new_commentaire, form_url: form_url, dossier: dossier } diff --git a/app/views/shared/dossiers/messages/_message_icon.html.haml b/app/views/shared/dossiers/messages/_message_icon.html.haml deleted file mode 100644 index 849337641..000000000 --- a/app/views/shared/dossiers/messages/_message_icon.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -- if commentaire.sent_by_system? - = image_tag('icons/mail.svg', class: 'person-icon', alt: '') -- elsif commentaire.sent_by?(connected_user) - = image_tag('icons/account-circle.svg', class: 'person-icon', alt: '') -- else - = image_tag('icons/blue-person.svg', class: 'person-icon', alt: '') - diff --git a/app/views/shared/dossiers/messages/_message_issuer.html.haml b/app/views/shared/dossiers/messages/_message_issuer.html.haml deleted file mode 100644 index 87a12da64..000000000 --- a/app/views/shared/dossiers/messages/_message_issuer.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- if commentaire.sent_by_system? - = t('views.shared.dossiers.messages.message_issuer.automatic_email') -- elsif commentaire.sent_by?(connected_user) - = t('views.shared.dossiers.messages.message_issuer.you') -- else - = commentaire.redacted_email diff --git a/app/views/users/dossiers/show/_latest_message.html.haml b/app/views/users/dossiers/show/_latest_message.html.haml index 73ec2e106..6fc81ff8e 100644 --- a/app/views/users/dossiers/show/_latest_message.html.haml +++ b/app/views/users/dossiers/show/_latest_message.html.haml @@ -4,7 +4,7 @@ %h3.tab-title= t('views.users.dossiers.show.latest_message.latest_message') .message.inverted-background - = render partial: "shared/dossiers/messages/message", locals: { commentaire: latest_message, connected_user: current_user, messagerie_seen_at: nil, show_reply_button: false } + = render Dossiers::MessageComponent.new(commentaire: latest_message, connected_user: current_user) = link_to messagerie_dossier_url(dossier, anchor: 'new_commentaire'), class: 'button send' do %span.icon.reply diff --git a/config/locales/en.yml b/config/locales/en.yml index e122e7020..bf6db58e3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -127,9 +127,6 @@ en: submit_dossier: Submit the file save_changes: Save the changes of the file messages: - message_issuer: - automatic_email: "Automatic email" - you: "You" form: send_message: "Send message" attachment_size: "(attachment size max : 20 Mo)" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2fcff2cae..177b44dd4 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -122,9 +122,6 @@ fr: submit_dossier: Déposer le dossier save_changes: Enregistrer les modifications du dossier messages: - message_issuer: - automatic_email: "Email automatique" - you: "Vous" form: send_message: "Envoyer le message" attachment_size: "(taille max : 20 Mo)" diff --git a/config/locales/views/instructeurs/en.yml b/config/locales/views/instructeurs/en.yml index ebd1f3cea..49ea80d96 100644 --- a/config/locales/views/instructeurs/en.yml +++ b/config/locales/views/instructeurs/en.yml @@ -1,7 +1,5 @@ en: instructeurs: - commentaires_controller: - alert_already_discarded: "Can not destroy message: it was already destroyed" procedure: archive_pending_html: Archive creation pending
(requested %{created_period} ago) archive_ready_html: Download archive
(requested %{generated_period} ago) diff --git a/config/locales/views/instructeurs/fr.yml b/config/locales/views/instructeurs/fr.yml index 2b5c8c035..153dbc679 100644 --- a/config/locales/views/instructeurs/fr.yml +++ b/config/locales/views/instructeurs/fr.yml @@ -1,7 +1,5 @@ fr: instructeurs: - commentaires_controller: - alert_already_discarded: Ce message a déjà été supprimé procedure: archive_pending_html: Archive en cours de création
(demandée il y a %{created_period}) archive_ready_html: Télécharger l’archive
(demandée il y a %{generated_period}) diff --git a/config/locales/views/shared/dossiers/en.yml b/config/locales/views/shared/dossiers/en.yml deleted file mode 100644 index 50fe2ce18..000000000 --- a/config/locales/views/shared/dossiers/en.yml +++ /dev/null @@ -1,9 +0,0 @@ -en: - shared: - dossiers: - messages: - message: - reply: Reply - guest: Guest - delete_button: Delete this message - confirm: Are you sure you want to delete this message ? diff --git a/config/locales/views/shared/dossiers/fr.yml b/config/locales/views/shared/dossiers/fr.yml deleted file mode 100644 index fe70e4cf7..000000000 --- a/config/locales/views/shared/dossiers/fr.yml +++ /dev/null @@ -1,9 +0,0 @@ -fr: - shared: - dossiers: - messages: - message: - reply: Répondre - guest: Invité - delete_button: Supprimer le message - confirm: Êtes-vous sûr de vouloir supprimer ce message ? diff --git a/config/locales/views/shared/en.yml b/config/locales/views/shared/en.yml index bb67c5377..f1bb0982c 100644 --- a/config/locales/views/shared/en.yml +++ b/config/locales/views/shared/en.yml @@ -20,6 +20,3 @@ en: already_user: "I already have an account" create: 'Create an account' signin: 'Sign in' - commentaires: - destroy: - deleted_body: Message deleted diff --git a/config/locales/views/shared/fr.yml b/config/locales/views/shared/fr.yml index 42f484605..4053b3cd3 100644 --- a/config/locales/views/shared/fr.yml +++ b/config/locales/views/shared/fr.yml @@ -20,6 +20,3 @@ fr: already_user: 'J’ai déjà un compte' create: 'Créer un compte' signin: 'Connexion' - commentaires: - destroy: - deleted_body: Message supprimé diff --git a/spec/views/shared/dossiers/messages/message.html.haml_spec.rb b/spec/components/dossiers/message_component_spec.rb similarity index 59% rename from spec/views/shared/dossiers/messages/message.html.haml_spec.rb rename to spec/components/dossiers/message_component_spec.rb index 00744a27a..9fb4b9d99 100644 --- a/spec/views/shared/dossiers/messages/message.html.haml_spec.rb +++ b/spec/components/dossiers/message_component_spec.rb @@ -1,21 +1,28 @@ -describe 'shared/dossiers/messages/message.html.haml', type: :view do - before { view.extend DossierHelper } - - subject { render 'shared/dossiers/messages/message.html.haml', commentaire: commentaire, messagerie_seen_at: seen_at, connected_user: dossier.user, show_reply_button: true } - +RSpec.describe Dossiers::MessageComponent, type: :component do + let(:component) do + described_class.new( + commentaire: commentaire, + connected_user: connected_user, + messagerie_seen_at: seen_at, + show_reply_button: true + ) + end let(:dossier) { create(:dossier, :en_construction) } let(:commentaire) { create(:commentaire, dossier: dossier) } + let(:connected_user) { dossier.user } let(:seen_at) { commentaire.created_at + 1.hour } + subject { render_inline(component).to_html } + it { is_expected.to have_button("Répondre") } - context "with a seen_at after commentaire created_at" do + context 'with a seen_at after commentaire created_at' do let(:seen_at) { commentaire.created_at + 1.hour } it { is_expected.not_to have_css(".highlighted") } end - context "with a seen_at after commentaire created_at" do + context 'with a seen_at after commentaire created_at' do let(:seen_at) { commentaire.created_at - 1.hour } it { is_expected.to have_css(".highlighted") } @@ -51,8 +58,8 @@ describe 'shared/dossiers/messages/message.html.haml', type: :view do let(:instructeur) { create(:instructeur) } let(:procedure) { create(:procedure) } let(:dossier) { create(:dossier, :en_construction, commentaires: [commentaire], procedure: procedure) } - subject { render 'shared/dossiers/messages/message.html.haml', commentaire: commentaire, messagerie_seen_at: seen_at, connected_user: instructeur, show_reply_button: true } - let(:form_url) { instructeur_commentaire_path(commentaire.dossier.procedure, commentaire.dossier, commentaire) } + let(:connected_user) { instructeur } + let(:form_url) { component.helpers.instructeur_commentaire_path(commentaire.dossier.procedure, commentaire.dossier, commentaire) } context 'on a procedure where commentaire had been written by connected instructeur' do let(:commentaire) { create(:commentaire, instructeur: instructeur, body: 'Second message') } @@ -64,7 +71,7 @@ describe 'shared/dossiers/messages/message.html.haml', type: :view do let(:commentaire) { create(:commentaire, instructeur: instructeur, body: 'Second message', discarded_at: 2.days.ago) } it { is_expected.not_to have_selector("form[action=\"#{form_url}\"]") } - it { is_expected.not_to have_selector(".rich-text", text: I18n.t(t('views.shared.commentaires.destroy.deleted_body'))) } + it { is_expected.to have_selector(".rich-text", text: component.t('.deleted_body')) } end context 'on a procedure where commentaire had been written by connected an user' do @@ -86,4 +93,50 @@ describe 'shared/dossiers/messages/message.html.haml', type: :view do end end end + + describe '#commentaire_from_guest?' do + let!(:guest) { create(:invite, dossier: dossier) } + + subject { component.send(:commentaire_from_guest?) } + + context 'when the commentaire sender is not a guest' do + let(:commentaire) { create(:commentaire, dossier: dossier, email: "michel@pref.fr") } + it { is_expected.to be false } + end + + context 'when the commentaire sender is a guest on this dossier' do + let(:commentaire) { create(:commentaire, dossier: dossier, email: guest.email) } + it { is_expected.to be true } + end + end + + describe '#commentaire_date' do + let(:present_date) { Time.zone.local(2018, 9, 2, 10, 5, 0) } + let(:creation_date) { present_date } + let(:commentaire) do + Timecop.freeze(creation_date) { create(:commentaire, email: "michel@pref.fr") } + end + + subject do + Timecop.freeze(present_date) { component.send(:commentaire_date) } + end + + it 'doesn’t include the creation year' do + expect(subject).to eq 'le 2 septembre à 10 h 05' + end + + context 'when displaying a commentaire created on a previous year' do + let(:creation_date) { present_date.prev_year } + it 'includes the creation year' do + expect(subject).to eq 'le 2 septembre 2017 à 10 h 05' + end + end + + context 'when formatting the first day of the month' do + let(:present_date) { Time.zone.local(2018, 9, 1, 10, 5, 0) } + it 'includes the ordinal' do + expect(subject).to eq 'le 1er septembre à 10 h 05' + end + end + end end diff --git a/spec/components/previews/dossiers/message_component_preview.rb b/spec/components/previews/dossiers/message_component_preview.rb new file mode 100644 index 000000000..e85ac03d1 --- /dev/null +++ b/spec/components/previews/dossiers/message_component_preview.rb @@ -0,0 +1,31 @@ +class Dossiers::MessageComponentPreview < ViewComponent::Preview + def with_default_commentaire + render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: user) + end + + def with_discarded_commentaire + render Dossiers::MessageComponent.new(commentaire: discarded_commentaire, connected_user: user) + end + + private + + def user + User.new email: "usager@example.com", locale: I18n.locale + end + + def commentaire + Commentaire.new body: 'Hello world!', email: user.email, dossier: dossier, created_at: 2.days.ago + end + + def discarded_commentaire + Commentaire.new body: 'Hello world!', email: user.email, dossier: dossier, created_at: 2.days.ago, discarded_at: 1.day.ago + end + + def dossier + Dossier.new(id: 47882, state: :en_instruction, procedure: procedure, user: user) + end + + def procedure + Procedure.new id: 1234, libelle: 'Dotation d’Équipement des Territoires Ruraux - Exercice 2019' + end +end diff --git a/spec/helpers/commentaire_helper_spec.rb b/spec/helpers/commentaire_helper_spec.rb index e54b1a860..13a0710a6 100644 --- a/spec/helpers/commentaire_helper_spec.rb +++ b/spec/helpers/commentaire_helper_spec.rb @@ -28,51 +28,4 @@ RSpec.describe CommentaireHelper, type: :helper do it { is_expected.to include('Répondre') } end end - - describe '.commentaire_is_from_guest' do - let(:dossier) { create(:dossier, :en_instruction) } - let!(:guest) { create(:invite, dossier: dossier) } - - subject { commentaire_is_from_guest(commentaire) } - - context 'when the commentaire sender is not a guest' do - let(:commentaire) { create(:commentaire, dossier: dossier, email: "michel@pref.fr") } - it { is_expected.to be false } - end - - context 'when the commentaire sender is a guest on this dossier' do - let(:commentaire) { create(:commentaire, dossier: dossier, email: guest.email) } - it { is_expected.to be true } - end - end - - describe '.commentaire_date' do - let(:present_date) { Time.zone.local(2018, 9, 2, 10, 5, 0) } - let(:creation_date) { present_date } - let(:commentaire) do - Timecop.freeze(creation_date) { create(:commentaire, email: "michel@pref.fr") } - end - - subject do - Timecop.freeze(present_date) { commentaire_date(commentaire) } - end - - it 'doesn’t include the creation year' do - expect(subject).to eq 'le 2 septembre à 10 h 05' - end - - context 'when displaying a commentaire created on a previous year' do - let(:creation_date) { present_date.prev_year } - it 'includes the creation year' do - expect(subject).to eq 'le 2 septembre 2017 à 10 h 05' - end - end - - context 'when formatting the first day of the month' do - let(:present_date) { Time.zone.local(2018, 9, 1, 10, 5, 0) } - it 'includes the ordinal' do - expect(subject).to eq 'le 1er septembre à 10 h 05' - end - end - end end From 53bc64f6e28ef531506aec09fd7c04a4640685c1 Mon Sep 17 00:00:00 2001 From: simon lehericey Date: Wed, 27 Apr 2022 09:49:09 +0200 Subject: [PATCH 29/31] change accessibility compliance declaration --- config/locales/links.en.yml | 4 ++-- config/locales/links.fr.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/links.en.yml b/config/locales/links.en.yml index 474d7b672..b3e41e3b4 100644 --- a/config/locales/links.en.yml +++ b/config/locales/links.en.yml @@ -12,8 +12,8 @@ en: url: "https://numerique.gouv.fr" footer: accessibilite: - label: "Accessibility: not compliant" - title: "Accessibility: not compliant" + label: "Accessibility: partially compliant" + title: "Accessibility: partially compliant" url: "https://doc.demarches-simplifiees.fr/declaration-daccessibilite" aide: label: "Help" diff --git a/config/locales/links.fr.yml b/config/locales/links.fr.yml index f55690670..a3d0ed9a5 100644 --- a/config/locales/links.fr.yml +++ b/config/locales/links.fr.yml @@ -12,8 +12,8 @@ fr: url: "https://numerique.gouv.fr" footer: accessibilite: - label: "Accessibilité : non conforme" - title: "Accessibilité : non conforme" + label: "Accessibilité : partiellement conforme" + title: "Accessibilité : partiellement conforme" url: "https://doc.demarches-simplifiees.fr/declaration-daccessibilite" aide: label: "Aide" From 55d77e33ed69e16dfaa04904cef5ccbeb36d0695 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 25 Apr 2022 15:50:33 +0200 Subject: [PATCH 30/31] feat(ApiEntreprise.recipient): always use SIRET of dinum Update config/env.example Co-authored-by: LeSim --- app/lib/api_entreprise/api.rb | 2 +- config/env.example | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/lib/api_entreprise/api.rb b/app/lib/api_entreprise/api.rb index 821c383af..297ad0669 100644 --- a/app/lib/api_entreprise/api.rb +++ b/app/lib/api_entreprise/api.rb @@ -120,7 +120,7 @@ class APIEntreprise::API # rubocop:disable DS/ApplicationName params = { context: "demarches-simplifiees.fr", - recipient: siret_or_siren, + recipient: ENV.fetch('API_ENTREPRISE_DEFAULT_SIRET'), object: "procedure_id: #{procedure_id}", non_diffusables: true } diff --git a/config/env.example b/config/env.example index c850f6de5..56276ac9a 100644 --- a/config/env.example +++ b/config/env.example @@ -157,3 +157,6 @@ INVISIBLE_CAPTCHA_SECRET="kikooloool" # Clamav antivirus usage CLAMAV_ENABLED="disabled" + +# Siret number used for API Entreprise, by default we use SIRET from dinum +API_ENTREPRISE_DEFAULT_SIRET="put_your_own_siret" From be090a1bec3a46c2acd4e91ea5b482e7ce9e2562 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 25 Apr 2022 17:38:30 +0200 Subject: [PATCH 31/31] feat(administrateur/procedure#create): allow admin to add a lien to the DPO, allow user to consult link to dpo. enhance spec on _procedure_footer.html Update spec/views/users/_procedure_footer.html.haml_spec.rb Co-authored-by: Pierre de La Morinerie --- .../administrateurs/procedures_controller.rb | 2 +- app/helpers/conservation_de_donnees_helper.rb | 4 +- app/helpers/procedure_helper.rb | 8 ++++ app/models/procedure.rb | 2 + app/validators/email_or_link_validator.rb | 7 ++++ .../procedures/_informations.html.haml | 7 ++++ app/views/users/_procedure_footer.html.haml | 31 ++++++++------- config/locales/models/procedure/fr.yml | 3 ++ .../views/users/procedure_footer/en.yml | 24 ++++++++++++ .../views/users/procedure_footer/fr.yml | 24 ++++++++++++ ...0220425140107_add_lien_dpo_to_procedure.rb | 5 +++ db/schema.rb | 3 +- spec/models/procedure_spec.rb | 7 ++++ .../users/_procedure_footer.html.haml_spec.rb | 39 +++++++++++++++++++ 14 files changed, 149 insertions(+), 17 deletions(-) create mode 100644 app/validators/email_or_link_validator.rb create mode 100644 config/locales/views/users/procedure_footer/en.yml create mode 100644 config/locales/views/users/procedure_footer/fr.yml create mode 100644 db/migrate/20220425140107_add_lien_dpo_to_procedure.rb diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index f40976d1d..2388508db 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -254,7 +254,7 @@ module Administrateurs end def procedure_params - editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :declarative_with_state, :logo, :auto_archive_on, :monavis_embed, :api_entreprise_token, :duree_conservation_dossiers_dans_ds, :zone_id] + editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :declarative_with_state, :logo, :auto_archive_on, :monavis_embed, :api_entreprise_token, :duree_conservation_dossiers_dans_ds, :zone_id, :lien_dpo] permited_params = if @procedure&.locked? params.require(:procedure).permit(*editable_params) else diff --git a/app/helpers/conservation_de_donnees_helper.rb b/app/helpers/conservation_de_donnees_helper.rb index e3aeeba18..634eb5559 100644 --- a/app/helpers/conservation_de_donnees_helper.rb +++ b/app/helpers/conservation_de_donnees_helper.rb @@ -7,7 +7,9 @@ module ConservationDeDonneesHelper def conservation_dans_ds(procedure) if procedure.duree_conservation_dossiers_dans_ds.present? - "Dans #{APPLICATION_NAME} : #{procedure.duree_conservation_dossiers_dans_ds} mois" + I18n.t('users.procedure_footer.legals.data_retention', + application_name: APPLICATION_NAME, + duree_conservation_dossiers_dans_ds: procedure.duree_conservation_dossiers_dans_ds) end end end diff --git a/app/helpers/procedure_helper.rb b/app/helpers/procedure_helper.rb index 5cfc94452..118169430 100644 --- a/app/helpers/procedure_helper.rb +++ b/app/helpers/procedure_helper.rb @@ -74,4 +74,12 @@ module ProcedureHelper .includes(:groupe_instructeur) .exists?(groupe_instructeur: current_instructeur.groupe_instructeurs) end + + def url_or_email_to_lien_dpo(procedure) + URI::MailTo.build([procedure.lien_dpo, "subject="]).to_s + rescue URI::InvalidComponentError + uri = URI.parse(procedure.lien_dpo) + return "//#{uri}" if uri.scheme.nil? + uri.to_s + end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 4c6f8f396..fad4b088a 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -28,6 +28,7 @@ # juridique_required :boolean default(TRUE) # libelle :string # lien_demarche :string +# lien_dpo :string # lien_notice :string # lien_site_web :string # monavis_embed :text @@ -266,6 +267,7 @@ class Procedure < ApplicationRecord validate :check_juridique validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false } validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION } + validates :lien_dpo, email_or_link: true, allow_nil: true validates_with MonAvisEmbedValidator FILE_MAX_SIZE = 20.megabytes diff --git a/app/validators/email_or_link_validator.rb b/app/validators/email_or_link_validator.rb new file mode 100644 index 000000000..7b0358256 --- /dev/null +++ b/app/validators/email_or_link_validator.rb @@ -0,0 +1,7 @@ +class EmailOrLinkValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + URI.parse(value) + rescue URI::InvalidURIError + record.errors.add(attribute, :invalid_uri_or_email) + end +end diff --git a/app/views/administrateurs/procedures/_informations.html.haml b/app/views/administrateurs/procedures/_informations.html.haml index c97170cda..0ec13a30b 100644 --- a/app/views/administrateurs/procedures/_informations.html.haml +++ b/app/views/administrateurs/procedures/_informations.html.haml @@ -57,6 +57,13 @@ = f.label :deliberation, 'Importer le texte' = text_upload_and_render f, @procedure.deliberation +%h3.header-subsection + RGPD +%p.notice + Pour certaines démarches, veuillez indiquer soit un mail le mail de contact de votre délégué à la protection des données, soit un lien web pointant vers les informations + += f.label :lien_dpo, 'Lien ou email pour contacter le Délégué à la Protection des Données (DPO)' += f.text_field :lien_dpo, class: 'form-control' %h3.header-subsection Notice explicative de la démarche %p.notice diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml index c1cdb13cb..465ea060d 100644 --- a/app/views/users/_procedure_footer.html.haml +++ b/app/views/users/_procedure_footer.html.haml @@ -4,7 +4,7 @@ - if service.present? .footer-row.footer-columns .footer-column - %p.footer-header Cette démarche est gérée par : + %p.footer-header= I18n.t('users.procedure_footer.managed_by.header') %ul %li = service.nom @@ -14,46 +14,49 @@ = string_to_html(service.adresse, wrapper_tag = 'span') .footer-column - %p.footer-header Poser une question sur votre dossier : + %p.footer-header= I18n.t('users.procedure_footer.contact.header') %ul %li - if dossier.present? && dossier.messagerie_available? - Directement - = link_to "par la messagerie", messagerie_dossier_path(dossier) + = I18n.t('users.procedure_footer.contact.in_app_mail.prefix') + = link_to I18n.t('users.procedure_footer.contact.in_app_mail.link'), messagerie_dossier_path(dossier) - else - Par email : + = I18n.t('users.procedure_footer.contact.email.prefix') = link_to service.email, "mailto:#{service.email}" - if service.telephone.present? %li - Par téléphone : + = I18n.t('users.procedure_footer.contact.phone.prefix') = link_to service.telephone, service.telephone_url %li - - horaires = "Horaires : #{formatted_horaires(service.horaires)}" + - horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}" = simple_format(horaires, {}, wrapper_tag: 'span') %li - Statistiques : - = link_to "voir les statistiques de la démarche", statistiques_path(procedure.path) + = I18n.t('users.procedure_footer.contact.stats.prefix') + = link_to I18n.t('users.procedure_footer.contact.stats.cta'), statistiques_path(procedure.path) - politiques = politiques_conservation_de_donnees(procedure) - if politiques.present? .footer-column - %p.footer-header Conservation des données : + %p.footer-header= I18n.t('users.procedure_footer.legals.header') %ul - politiques.each do |politique| %li= politique - %p.mt-2.footer-header Cadre juridique : - %ul - if procedure.deliberation.attached? %li = link_to url_for(procedure.deliberation), target: '_blank', rel: 'noopener' do - = "Texte cadrant la demande d'information" + = I18n.t("users.procedure_footer.legals.terms") - else %li - = link_to "Texte juridique la demande d'information", procedure.cadre_juridique, target: '_blank', rel: 'noopener' + = link_to I18n.t("users.procedure_footer.legals.terms"), procedure.cadre_juridique, target: '_blank', rel: 'noopener' + + - if procedure.lien_dpo.present? + %li + = link_to url_or_email_to_lien_dpo(procedure), target: '_blank', rel: 'noopener' do + = I18n.t("users.procedure_footer.legals.dpo") = render partial: 'users/general_footer_row', locals: { dossier: dossier } diff --git a/config/locales/models/procedure/fr.yml b/config/locales/models/procedure/fr.yml index 752936742..1c3f1b9e1 100644 --- a/config/locales/models/procedure/fr.yml +++ b/config/locales/models/procedure/fr.yml @@ -17,6 +17,7 @@ fr: declarative_with_state/en_instruction: En instruction declarative_with_state/accepte: Accepté api_particulier_token: Jeton API Particulier + lien_dpo: Contact du DPO errors: models: procedure: @@ -27,3 +28,5 @@ fr: format: 'Le champ %{message}' draft_types_de_champ_private: format: 'L’annotation privée %{message}' + lien_dpo: + invalid_uri_or_email: "Veuillez saisir un mail ou un lien" diff --git a/config/locales/views/users/procedure_footer/en.yml b/config/locales/views/users/procedure_footer/en.yml new file mode 100644 index 000000000..31c54b19d --- /dev/null +++ b/config/locales/views/users/procedure_footer/en.yml @@ -0,0 +1,24 @@ +en: + users: + procedure_footer: + managed_by: + header: 'This procedure is managed by :' + contact: + header: 'Ask a question about your file :' + in_app_mail: + prefix: 'Directly :' + link: "via the chat" + email: + prefix: 'By mail :' + phone: + prefix: 'By phone :' + schedule: + prefix: 'Hours : ' + stats: + prefix: 'Stats :' + cta: "see the procedure's stats" + legals: + header: "Legals :" + data_retention: "Within %{application_name} : %{duree_conservation_dossiers_dans_ds} months" + terms: "Laws regarding this data collection" + dpo: "Contact the Data Protection Officer" diff --git a/config/locales/views/users/procedure_footer/fr.yml b/config/locales/views/users/procedure_footer/fr.yml new file mode 100644 index 000000000..b8a085703 --- /dev/null +++ b/config/locales/views/users/procedure_footer/fr.yml @@ -0,0 +1,24 @@ +fr: + users: + procedure_footer: + managed_by: + header: 'Cette démarche est gérée par :' + contact: + header: 'Poser une question sur votre dossier :' + in_app_mail: + prefix: 'Directement :' + link: "par la messagerie" + email: + prefix: 'Par email :' + phone: + prefix: 'Par téléphone :' + schedule: + prefix: 'Horaires : ' + stats: + prefix: 'Statistiques :' + cta: "voir les statistiques de la démarche" + legals: + header: "Cadre juridique :" + data_retention: "Dans %{application_name} : %{duree_conservation_dossiers_dans_ds} mois" + terms: "Texte cadrant la demande d'information" + dpo: "Contacter le Délégué à la Protection des Données" diff --git a/db/migrate/20220425140107_add_lien_dpo_to_procedure.rb b/db/migrate/20220425140107_add_lien_dpo_to_procedure.rb new file mode 100644 index 000000000..891b0215a --- /dev/null +++ b/db/migrate/20220425140107_add_lien_dpo_to_procedure.rb @@ -0,0 +1,5 @@ +class AddLienDpoToProcedure < ActiveRecord::Migration[6.1] + def change + add_column :procedures, :lien_dpo, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index d59c95dd9..63fc7466c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_04_07_081538) do +ActiveRecord::Schema.define(version: 2022_04_25_140107) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -645,6 +645,7 @@ ActiveRecord::Schema.define(version: 2022_04_07_081538) do t.boolean "juridique_required", default: true t.string "libelle" t.string "lien_demarche" + t.string "lien_dpo" t.string "lien_notice" t.string "lien_site_web" t.text "monavis_embed" diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 48d87d666..758791972 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -1185,6 +1185,13 @@ describe Procedure do end end + describe 'lien_dpo' do + it { expect(build(:procedure).valid?).to be(true) } + it { expect(build(:procedure, lien_dpo: 'dpo@ministere.amere').valid?).to be(true) } + it { expect(build(:procedure, lien_dpo: 'https://legal.fr/contact_dpo').valid?).to be(true) } + it { expect(build(:procedure, lien_dpo: 'askjdlad l akdj asd ').valid?).to be(false) } + end + private def create_dossier_with_pj_of_size(size, procedure) diff --git a/spec/views/users/_procedure_footer.html.haml_spec.rb b/spec/views/users/_procedure_footer.html.haml_spec.rb index 4fc40831d..87605fd18 100644 --- a/spec/views/users/_procedure_footer.html.haml_spec.rb +++ b/spec/views/users/_procedure_footer.html.haml_spec.rb @@ -27,4 +27,43 @@ describe 'users/procedure_footer.html.haml', type: :view do it { is_expected.to have_link("Accessibilité") } it { is_expected.not_to have_text('téléphone') } end + + describe '#cadre_juridique' do + context 'when an external link is provided' do + before { dossier.procedure.update(cadre_juridique: "http://google.fr") } + it { is_expected.to have_link("Texte cadrant la demande d'information", href: 'http://google.fr') } + end + + context 'when there is deliberation attached' do + before { dossier.procedure.update(cadre_juridique: nil, deliberation: fixture_file_upload('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf')) } + it { is_expected.to have_link("Texte cadrant la demande d'information") } + end + end + + describe '#lien_dpo' do + context "when there is not lien_dpo" do + before { dossier.procedure.update(lien_dpo: nil) } + it { is_expected.not_to have_text('Contacter le Délégué à la Protection des Données') } + end + + context "when there is a lien_dpo with an email" do + before { dossier.procedure.update(lien_dpo: 'dpo@beta.gouv.fr') } + it { is_expected.to have_selector('a[href="mailto:dpo@beta.gouv.fr?subject="]') } + end + + context "when there is a lien_dpo with a schemaless link" do + before { dossier.procedure.update(lien_dpo: 'beta.gouv.fr') } + it { is_expected.to have_link('Contacter le Délégué à la Protection des Données', href: '//beta.gouv.fr') } + end + + context "when there is a lien_dpo with a link with http:// schema" do + before { dossier.procedure.update(lien_dpo: 'http://beta.gouv.fr') } + it { is_expected.to have_link('Contacter le Délégué à la Protection des Données', href: 'http://beta.gouv.fr') } + end + + context "when there is a lien_dpo with a link with https:// schema" do + before { dossier.procedure.update(lien_dpo: 'https://beta.gouv.fr') } + it { is_expected.to have_link('Contacter le Délégué à la Protection des Données', href: 'https://beta.gouv.fr') } + end + end end