Merge pull request #7182 from betagouv/main

2022-04-21-01
This commit is contained in:
Kara Diaby 2022-04-21 14:00:05 +02:00 committed by GitHub
commit 3f0cb4d12d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 369 additions and 209 deletions

View file

@ -71,7 +71,6 @@ gem 'rack-attack'
gem 'rails' gem 'rails'
gem 'rails-i18n' # Locales par défaut gem 'rails-i18n' # Locales par défaut
gem 'rake-progressbar', require: false gem 'rake-progressbar', require: false
gem 'react-rails'
gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325) gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325)
gem 'rgeo-geojson' gem 'rgeo-geojson'
gem 'rqrcode' gem 'rqrcode'

View file

@ -121,10 +121,6 @@ GEM
axlsx_styler (1.1.0) axlsx_styler (1.1.0)
activesupport (>= 3.1) activesupport (>= 3.1)
caxlsx (>= 2.0.2) caxlsx (>= 2.0.2)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
execjs (~> 2.0)
bcrypt (3.1.16) bcrypt (3.1.16)
better_html (1.0.16) better_html (1.0.16)
actionview (>= 4.0) actionview (>= 4.0)
@ -173,7 +169,6 @@ GEM
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
concurrent-ruby (1.1.10) concurrent-ruby (1.1.10)
connection_pool (2.2.3)
content_disposition (1.0.0) content_disposition (1.0.0)
crack (0.4.5) crack (0.4.5)
rexml rexml
@ -238,7 +233,6 @@ GEM
ethon (0.15.0) ethon (0.15.0)
ffi (>= 1.15.0) ffi (>= 1.15.0)
excon (0.79.0) excon (0.79.0)
execjs (2.7.0)
factory_bot (6.1.0) factory_bot (6.1.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
faraday (1.8.0) faraday (1.8.0)
@ -572,12 +566,6 @@ GEM
rb-fsevent (0.10.4) rb-fsevent (0.10.4)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
react-rails (2.6.1)
babel-transpiler (>= 0.7.0)
connection_pool
execjs
railties (>= 3.2)
tilt
regexp_parser (2.1.0) regexp_parser (2.1.0)
request_store (1.5.0) request_store (1.5.0)
rack (>= 1.4) rack (>= 1.4)
@ -885,7 +873,6 @@ DEPENDENCIES
rails-erd rails-erd
rails-i18n rails-i18n
rake-progressbar rake-progressbar
react-rails
rexml rexml
rgeo-geojson rgeo-geojson
rqrcode rqrcode

View file

@ -15,7 +15,7 @@
} }
} }
.form [data-react-class='MapEditor'] [data-reach-combobox-input] { .form [data-react-component-value='MapEditor'] [data-reach-combobox-input] {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -488,13 +488,13 @@
} }
} }
[data-react-class]:not([data-react-class^="ComboMultiple"]) { [data-react-component-value]:not([data-react-component-value^="ComboMultiple"]) {
[data-reach-combobox-input]:not(.no-margin) { [data-reach-combobox-input]:not(.no-margin) {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
} }
} }
[data-react-class^="ComboMultiple"] { [data-react-component-value^="ComboMultiple"] {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
[data-reach-combobox-input] { [data-reach-combobox-input] {

View file

@ -9,7 +9,7 @@
margin-left: 16px; margin-left: 16px;
} }
[data-react-class^="ComboMultiple"] { [data-react-component-value^="ComboMultiple"] {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] { [data-reach-combobox-token-list] {

View file

@ -62,7 +62,7 @@
text-align: center; text-align: center;
} }
[data-react-class^="ComboMultiple"] { [data-react-component-value^="ComboMultiple"] {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] { [data-reach-combobox-token-list] {

View file

@ -1,6 +1,10 @@
module ApplicationHelper module ApplicationHelper
include SanitizeUrl include SanitizeUrl
def html_lang
I18n.locale.to_s
end
def sanitize_url(url) def sanitize_url(url)
if !url.nil? if !url.nil?
super(url, schemes: ['http', 'https'], replace_evil_with: root_url) super(url, schemes: ['http', 'https'], replace_evil_with: root_url)
@ -26,6 +30,10 @@ module ApplicationHelper
class_names.join(' ') class_names.join(' ')
end end
def react_component(name, props = {}, html = {})
tag.div(**html.merge(data: { controller: 'react', react_component_value: name, react_props_value: props.to_json }))
end
def render_to_element(selector, partial:, outer: false, locals: {}) def render_to_element(selector, partial:, outer: false, locals: {})
method = outer ? 'outerHTML' : 'innerHTML' method = outer ? 'outerHTML' : 'innerHTML'
html = escape_javascript(render partial: partial, locals: locals) html = escape_javascript(render partial: partial, locals: locals)

View file

@ -0,0 +1,70 @@
import { Controller } from '@hotwired/stimulus';
import React, { lazy, Suspense, FunctionComponent, StrictMode } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import invariant from 'tiny-invariant';
type Props = Record<string, unknown>;
type Loader = () => Promise<{ default: FunctionComponent<Props> }>;
const componentsRegistry = new Map<string, FunctionComponent<Props>>();
export function registerComponents(components: Record<string, Loader>): void {
for (const [className, loader] of Object.entries(components)) {
componentsRegistry.set(className, LoadableComponent(loader));
}
}
// Initialize React components when their markup appears into the DOM.
//
// Example:
// <div data-controller="react" data-react-component-value="ComboMultiple" data-react-props-value="{}"></div>
//
export class ReactController extends Controller {
static values = {
component: String,
props: Object
};
declare readonly componentValue: string;
declare readonly propsValue: Props;
connect(): void {
this.mountComponent(this.element as HTMLElement);
}
disconnect(): void {
unmountComponentAtNode(this.element as HTMLElement);
}
private mountComponent(node: HTMLElement): void {
const componentName = this.componentValue;
const props = this.propsValue;
const Component = this.getComponent(componentName);
invariant(
Component,
`Cannot find a React component with class "${componentName}"`
);
render(
<StrictMode>
<Component {...props} />
</StrictMode>,
node
);
}
private getComponent(componentName: string): FunctionComponent<Props> | null {
return componentsRegistry.get(componentName) ?? null;
}
}
const Spinner = () => <div className="spinner left" />;
function LoadableComponent(loader: Loader): FunctionComponent<Props> {
const LazyComponent = lazy(loader);
const Component: FunctionComponent<Props> = (props: Props) => (
<Suspense fallback={<Spinner />}>
<LazyComponent {...props} />
</Suspense>
);
return Component;
}

View file

@ -2,6 +2,7 @@ import '../shared/polyfills';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
import * as ActiveStorage from '@rails/activestorage'; import * as ActiveStorage from '@rails/activestorage';
import 'whatwg-fetch'; // window.fetch polyfill import 'whatwg-fetch'; // window.fetch polyfill
import { Application } from '@hotwired/stimulus';
import '../shared/page-update-event'; import '../shared/page-update-event';
import '../shared/activestorage/ujs'; import '../shared/activestorage/ujs';
@ -12,6 +13,11 @@ import '../shared/franceconnect';
import '../shared/toggle-target'; import '../shared/toggle-target';
import '../shared/ujs-error-handling'; import '../shared/ujs-error-handling';
import {
ReactController,
registerComponents
} from '../controllers/react_controller';
import '../new_design/dropdown'; import '../new_design/dropdown';
import '../new_design/form-validation'; import '../new_design/form-validation';
import '../new_design/procedure-context'; import '../new_design/procedure-context';
@ -46,37 +52,23 @@ import {
showNewAccountPasswordConfirmation showNewAccountPasswordConfirmation
} from '../new_design/fc-fusion'; } from '../new_design/fc-fusion';
import { registerComponents({
registerReactComponents, Chartkick: () => import('../components/Chartkick'),
Loadable ComboAdresseSearch: () => import('../components/ComboAdresseSearch'),
} from '../shared/register-react-components'; ComboAnnuaireEducationSearch: () =>
import('../components/ComboAnnuaireEducationSearch'),
registerReactComponents({ ComboCommunesSearch: () => import('../components/ComboCommunesSearch'),
Chartkick: Loadable(() => import('../components/Chartkick')), ComboDepartementsSearch: () =>
ComboAdresseSearch: Loadable(() => import('../components/ComboDepartementsSearch'),
import('../components/ComboAdresseSearch') ComboMultipleDropdownList: () =>
), import('../components/ComboMultipleDropdownList'),
ComboAnnuaireEducationSearch: Loadable(() => ComboMultiple: () => import('../components/ComboMultiple'),
import('../components/ComboAnnuaireEducationSearch') ComboPaysSearch: () => import('../components/ComboPaysSearch'),
), ComboRegionsSearch: () => import('../components/ComboRegionsSearch'),
ComboCommunesSearch: Loadable(() => MapEditor: () => import('../components/MapEditor'),
import('../components/ComboCommunesSearch') MapReader: () => import('../components/MapReader'),
), Trix: () => import('../components/Trix'),
ComboDepartementsSearch: Loadable(() => TypesDeChampEditor: () => import('../components/TypesDeChampEditor')
import('../components/ComboDepartementsSearch')
),
ComboMultipleDropdownList: Loadable(() =>
import('../components/ComboMultipleDropdownList')
),
ComboMultiple: Loadable(() => import('../components/ComboMultiple')),
ComboPaysSearch: Loadable(() => import('../components/ComboPaysSearch')),
ComboRegionsSearch: Loadable(() =>
import('../components/ComboRegionsSearch')
),
MapEditor: Loadable(() => import('../components/MapEditor')),
MapReader: Loadable(() => import('../components/MapReader')),
Trix: Loadable(() => import('../components/Trix')),
TypesDeChampEditor: Loadable(() => import('../components/TypesDeChampEditor'))
}); });
// This is the global application namespace where we expose helpers used from rails views // This is the global application namespace where we expose helpers used from rails views
@ -98,5 +90,8 @@ const DS = {
Rails.start(); Rails.start();
ActiveStorage.start(); ActiveStorage.start();
const Stimulus = Application.start();
Stimulus.register('react', ReactController);
// Expose globals // Expose globals
window.DS = window.DS || DS; window.DS = window.DS || DS;

View file

@ -1,97 +0,0 @@
import React, { Suspense, lazy, createElement, ComponentClass } from 'react';
import { render } from 'react-dom';
// This attribute holds the name of component which should be mounted
// example: `data-react-class="MyApp.Items.EditForm"`
const CLASS_NAME_ATTR = 'data-react-class';
// This attribute holds JSON stringified props for initializing the component
// example: `data-react-props="{\"item\": { \"id\": 1, \"name\": \"My Item\"} }"`
const PROPS_ATTR = 'data-react-props';
const CLASS_NAME_SELECTOR = `[${CLASS_NAME_ATTR}]`;
// helper method for the mount and unmount methods to find the
// `data-react-class` DOM elements
function findDOMNodes(searchSelector?: string): NodeListOf<HTMLDivElement> {
const [selector, parent] = getSelector(searchSelector);
return parent.querySelectorAll<HTMLDivElement>(selector);
}
function getSelector(searchSelector?: string): [string, Document] {
switch (typeof searchSelector) {
case 'undefined':
return [CLASS_NAME_SELECTOR, document];
case 'object':
return [CLASS_NAME_SELECTOR, searchSelector];
case 'string':
return [
['', ' ']
.map(
(separator) => `${searchSelector}${separator}${CLASS_NAME_SELECTOR}`
)
.join(', '),
document
];
}
}
class ReactComponentRegistry {
#components;
constructor(components: Record<string, ComponentClass>) {
this.#components = components;
}
getConstructor(className: string | null) {
return className ? this.#components[className] : null;
}
mountComponents(searchSelector?: string) {
const nodes = findDOMNodes(searchSelector);
for (const node of nodes) {
const className = node.getAttribute(CLASS_NAME_ATTR);
const ComponentClass = this.getConstructor(className);
const propsJson = node.getAttribute(PROPS_ATTR);
const props = propsJson && JSON.parse(propsJson);
if (!ComponentClass) {
const message = `Cannot find component: "${className}"`;
console?.log(
`%c[react-rails] %c${message} for element`,
'font-weight: bold',
'',
node
);
throw new Error(
`${message}. Make sure your component is available to render.`
);
} else {
render(createElement(ComponentClass, props), node);
}
}
}
}
const Loader = () => <div className="spinner left" />;
export function Loadable(loader: () => Promise<{ default: ComponentClass }>) {
const LazyComponent = lazy(loader);
return function PureComponent(props: Record<string, unknown>) {
return (
<Suspense fallback={<Loader />}>
<LazyComponent {...props} />
</Suspense>
);
};
}
export function registerReactComponents(
components: Record<string, ComponentClass>
) {
const registry = new ReactComponentRegistry(components);
addEventListener('ds:page:update', () => registry.mountComponents());
}

View file

@ -5,37 +5,44 @@
html: { class: 'form' } do |f| html: { class: 'form' } do |f|
= f.label :routing_criteria_name do = f.label :routing_criteria_name do
Libellé du routage = t('.routing.title')
%p.notice Ce texte apparaitra sur le formulaire usager comme le libellé dune liste %p.notice
= f.text_field :routing_criteria_name, placeholder: 'ex. Votre ville', required: true = t('.routing.notice')
= f.submit 'Renommer', class: 'button primary send' = f.text_field :routing_criteria_name, placeholder: t('.add_a_group.placeholder'), required: true
= f.submit t('.button.rename'), class: 'button primary send'
.card .card
.card-title Gestion des Groupes .card-title
= t('.group_management.title')
= form_for :groupe_instructeur, html: { class: 'form' } do |f| = form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do = f.label :label do
Ajouter un groupe = t('.add_a_group.title')
%p.notice Ce groupe sera un choix de la liste « #{procedure.routing_criteria_name} » . %p.notice
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true = t('.add_a_group.notice', routing_criteria_name: procedure.routing_criteria_name)
= f.submit 'Ajouter le groupe', class: "button primary send" = f.text_field :label, placeholder: t('.add_a_group.placeholder'), required: true
= f.submit t('.button.add_group'), class: "button primary send"
- csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE - csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE
- if procedure.publiee? - if procedure.publiee?
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do = form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV" = label_tag t('.csv_import.title')
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants. %p.notice
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)} = t('.csv_import.notice_1')
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/csv/#{I18n.locale}/import-groupe-test.csv" %p.notice
= t('.csv_import.notice_2', csv_max_size: number_to_human_size(csv_max_size))
%p.mt-2.mb-2= link_to t('.csv_import.download_exemple'), "/csv/#{I18n.locale}/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1" = file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." } = submit_tag t('.csv_import.import_file'), class: 'button primary send', data: { disable_with: "Envoi..." }
- else - else
%p.mt-4.form.bold.mb-2.text-lg Importer par fichier CSV %p.mt-4.form.bold.mb-2.text-lg
%p.notice Limport dinstructeurs par fichier CSV est disponible une fois la démarche publiée = t('.csv_import.title')
%p.notice
= t('.csv_import.import_file_procedure_not_published')
%table.table.mt-2 %table.table.mt-2
%thead %thead
%tr %tr
// i18n-tasks-use t('.existing_groupe')
%th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count) %th{ colspan: 2 }= t(".existing_groupe", count: groupes_instructeurs.total_count)
%th %th
- if groupe_instructeurs_count > 1 - if groupe_instructeurs_count > 1
@ -44,18 +51,18 @@
- groupes_instructeurs.each do |group| - groupes_instructeurs.each do |group|
%tr %tr
%td= group.label %td= group.label
%td.actions= link_to "voir", admin_procedure_groupe_instructeur_path(procedure, group) %td.actions= link_to t('.view'), admin_procedure_groupe_instructeur_path(procedure, group)
- if groupes_instructeurs.many? - if groupes_instructeurs.many?
- if group.dossiers.empty? - if group.dossiers.empty?
%td.actions %td.actions
= link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: "Êtes-vous sûr de vouloir supprimer le groupe « #{group.label} » ?" }} do = link_to admin_procedure_groupe_instructeur_path(procedure, group), { method: :delete, class: 'button', data: { confirm: t('.group_management.delete_confirmation', group_name: group.label) }} do
%span.icon.delete %span.icon.delete
supprimer ce groupe = t('.group_management.delete')
- else - else
%td.actions %td.actions
= link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(procedure, group), class: 'button', title:'Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer' do = link_to reaffecter_dossiers_admin_procedure_groupe_instructeur_path(procedure, group), class: 'button', title: t('.group_management.move_folders_confirmation') do
%span.icon.follow %span.icon.follow
déplacer les dossiers = t('.group_management.move_folders')
= paginate groupes_instructeurs = paginate groupes_instructeurs

View file

@ -1,5 +1,6 @@
.card .card
.card-title Routage .card-title
= t('.title')
- if !procedure.routee? - if !procedure.routee?
%p.notice= t('.notice_html') %p.notice= t('.notice_html')

View file

@ -1,8 +1,8 @@
- if @procedure.routee? - if @procedure.routee?
= render partial: 'administrateurs/breadcrumbs', = render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path), locals: { steps: [link_to(t('.procedures'), admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)), link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] } t('.instructors_group')] }
- else - else
= render partial: 'administrateurs/breadcrumbs', = render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path), locals: { steps: [link_to('Démarches', admin_procedures_path),

View file

@ -10,7 +10,6 @@
.card-title Réaffectation des dossiers du groupe « #{@groupe_instructeur.label} » .card-title Réaffectation des dossiers du groupe « #{@groupe_instructeur.label} »
%p %p
Le groupe « #{@groupe_instructeur.label} » contient des dossiers. Afin de procéder à sa suppression, vous devez réaffecter ses dossiers à un autre groupe instructeur. Le groupe « #{@groupe_instructeur.label} » contient des dossiers. Afin de procéder à sa suppression, vous devez réaffecter ses dossiers à un autre groupe instructeur.
%table.table.mt-2 %table.table.mt-2
%thead %thead
%tr %tr

View file

@ -5,74 +5,75 @@
'Notifications'] } 'Notifications'] }
.container .container
%h1 Notifications par email %h1
= t('.title')
= form_for @assign_to, url: update_email_notifications_instructeur_procedure_path(@procedure), html: { class: 'form' } do |form| = form_for @assign_to, url: update_email_notifications_instructeur_procedure_path(@procedure), html: { class: 'form' } do |form|
.explication .explication
Configurez sur cette page les notifications que vous souhaitez recevoir par email pour cette démarche. = t('.subtitle')
= form.label :email_notification, "Recevoir une notification à chaque dossier déposé" = form.label :email_notification, t('.for_each_file_submitted.title')
%p.notice %p.notice
Cet email vous signale le dépôt dun nouveau dossier. = t('.for_each_file_submitted.notice_1')
%p.notice %p.notice
Il est envoyé à chaque fois qu'un usager dépose un dossier. = t('.for_each_file_submitted.notice_2')
.radios .radios
%label %label
= form.radio_button :instant_email_dossier_notifications_enabled, true = form.radio_button :instant_email_dossier_notifications_enabled, true
Oui = t('.utils.positive')
%label %label
= form.radio_button :instant_email_dossier_notifications_enabled, false = form.radio_button :instant_email_dossier_notifications_enabled, false
Non = t('.utils.negative')
= form.label :email_notification, "Recevoir une notification à chaque message déposé" = form.label :email_notification, t('.for_each_message_submitted.title')
%p.notice %p.notice
Cet email vous signale le dépôt dun nouveau message sur vos dossiers suivis. = t('.for_each_message_submitted.notice_1')
%p.notice %p.notice
Il est envoyé à chaque fois qu'un usager dépose un message. = t('.for_each_message_submitted.notice_2')
.radios .radios
%label %label
= form.radio_button :instant_email_message_notifications_enabled, true = form.radio_button :instant_email_message_notifications_enabled, true
Oui = t('.utils.positive')
%label %label
= form.radio_button :instant_email_message_notifications_enabled, false = form.radio_button :instant_email_message_notifications_enabled, false
Non = t('.utils.negative')
= form.label :email_notification, "Recevoir une notification quotidienne" = form.label :email_notification, t('.daily_notifications.title')
%p.notice %p.notice
Cet email vous signale le dépôt de nouveaux dossiers sur cette démarche, ou des changements sur vos dossiers suivis. = t('.daily_notifications.notice_1')
%p.notice %p.notice
Il est envoyé une fois par jour, du lundi au samedi, vers 10 h du matin. = t('.daily_notifications.notice_2')
.radios .radios
%label %label
= form.radio_button :daily_email_notifications_enabled, true = form.radio_button :daily_email_notifications_enabled, true
Oui = t('.utils.positive')
%label %label
= form.radio_button :daily_email_notifications_enabled, false = form.radio_button :daily_email_notifications_enabled, false
Non = t('.utils.negative')
= form.label :email_notification, "Recevoir un récapitulatif hebdomadaire" = form.label :email_notification, t('.hebdo_recap.title')
%p.notice %p.notice
Cet email récapitule lactivité de la semaine sur lensemble de vos démarches. = t('.hebdo_recap.notice_1')
%p.notice %p.notice
Il est envoyé chaque semaine le lundi matin. = t('.hebdo_recap.notice_2')
.radios .radios
%label %label
= form.radio_button :weekly_email_notifications_enabled, true = form.radio_button :weekly_email_notifications_enabled, true
Oui = t('.utils.positive')
%label %label
= form.radio_button :weekly_email_notifications_enabled, false = form.radio_button :weekly_email_notifications_enabled, false
Non = t('.utils.negative')
.send-wrapper .send-wrapper
= link_to "Revenir à la procédure", instructeur_procedure_path(@procedure), class: 'button mr-1' = link_to t('.buttons.back_to_procedure'), instructeur_procedure_path(@procedure), class: 'button mr-1'
= form.submit "Enregistrer", class: "button primary" = form.submit t('.buttons.save'), class: "button primary"

View file

@ -2,10 +2,12 @@
= render partial: 'administrateurs/breadcrumbs', = render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)), locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
'Contacter les usagers (brouillon)'] } t('.contact_users')] }
.messagerie.container .messagerie.container
- if @email_usagers_dossiers.present? - if @email_usagers_dossiers.present?
%p.notice.mb-2.mt-4 Vous allez envoyer un message à #{pluralize(@dossiers_count, 'personne')} dont les dossiers sont en brouillon, dans les groupes instructeurs : #{@groupe_instructeurs.join(', ')}. %p.notice.mb-2.mt-4
= t('.notice', dossiers_count: pluralize(@dossiers_count, 'personne'), groupe_instructeurs: @groupe_instructeurs.join(', '))
= render partial: 'shared/dossiers/messages/form', locals: { commentaire: @commentaire, form_url: create_multiple_commentaire_instructeur_procedure_path(@procedure), disable_piece_jointe: true } = render partial: 'shared/dossiers/messages/form', locals: { commentaire: @commentaire, form_url: create_multiple_commentaire_instructeur_procedure_path(@procedure), disable_piece_jointe: true }
- if @bulk_messages.present? - if @bulk_messages.present?

View file

@ -3,6 +3,6 @@
= render partial: 'administrateurs/breadcrumbs', = render partial: 'administrateurs/breadcrumbs',
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)), locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)),
'Statistiques'] } t('.title')] }
= render partial: 'shared/procedures/stats', locals: { title: title } = render partial: 'shared/procedures/stats', locals: { title: title }

View file

@ -39,7 +39,7 @@
- if nav_bar_profile == :user - if nav_bar_profile == :user
%ul.header-tabs %ul.header-tabs
%li %li
= active_link_to "Dossiers", dossiers_path, active: :inclusive, class: 'tab-link' = active_link_to t('.files'), dossiers_path, active: :inclusive, class: 'tab-link'
- if current_user.expert && current_expert.avis_summary[:total] > 0 - if current_user.expert && current_expert.avis_summary[:total] > 0
= render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert }

View file

@ -1,5 +1,5 @@
!!! 5 !!! 5
%html{ lang: "fr", class: yield(:root_class) } %html{ lang: html_lang, class: yield(:root_class) }
%head %head
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }

View file

@ -2,5 +2,7 @@
= mail_to CONTACT_EMAIL do = mail_to CONTACT_EMAIL do
%span.icon.mail %span.icon.mail
.dropdown-description .dropdown-description
%span.help-dropdown-title Contact technique %span.help-dropdown-title
%p Envoyez nous un message à #{CONTACT_EMAIL}. = t('help_dropdown.technical_contact_title')
%p
= t('help_dropdown.technical_contact_description', contact_email: CONTACT_EMAIL)

View file

@ -2,5 +2,8 @@
= link_to FAQ_URL, target: "_blank", rel: "noopener" do = link_to FAQ_URL, target: "_blank", rel: "noopener" do
%span.icon.help %span.icon.help
.dropdown-description .dropdown-description
%span.help-dropdown-title Un problème avec le site ? %span.help-dropdown-title
%p Trouvez votre réponse dans laide en ligne. = t('help_dropdown.problem_title')
%p
= t('help_dropdown.problem_description')

View file

@ -49,6 +49,7 @@ module.exports = function (api) {
'@babel/plugin-syntax-dynamic-import', '@babel/plugin-syntax-dynamic-import',
isTestEnv && 'babel-plugin-dynamic-import-node', isTestEnv && 'babel-plugin-dynamic-import-node',
'@babel/plugin-transform-destructuring', '@babel/plugin-transform-destructuring',
['@babel/plugin-transform-typescript', { allowDeclareFields: true }],
[ [
'@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-class-properties',
{ {

View file

@ -34,6 +34,11 @@ en:
custom_message: 'If you are a human, ignore this field' custom_message: 'If you are a human, ignore this field'
help: 'Help' help: 'Help'
help_dropdown:
problem_title: A problem with the website ?
problem_description: Find your answer in the online help.
technical_contact_title: Technical contact
technical_contact_description: Send us a message to %{contact_email}.
utils: utils:
'yes': Yes 'yes': Yes
'no': No 'no': No

View file

@ -22,8 +22,12 @@
fr: fr:
invisible_captcha: invisible_captcha:
custom_message: 'Si vous êtes un humain, veuillez ignorer ce champs' custom_message: 'Si vous êtes un humain, veuillez ignorer ce champs'
help: 'Aide' help: 'Aide'
help_dropdown:
problem_title: Un problème avec le site ?
problem_description: Trouvez votre réponse dans laide en ligne.
technical_contact_title: Contact technique
technical_contact_description: Envoyez nous un message à %{contact_email}.
utils: utils:
'yes': Oui 'yes': Oui
'no': Non 'no': Non

View file

@ -1,4 +1,4 @@
fr: en:
activerecord: activerecord:
attributes: attributes:
procedure_presentation: procedure_presentation:

View file

@ -0,0 +1,67 @@
en:
administrateurs:
experts_procedures:
create:
experts_assignment:
one: "The expert %{value} was assigned to the procedure n° %{procedure}"
other: "The experts %{value} were assigned to the procedure n° %{procedure}"
groupe_instructeurs:
index:
procedures: Procedures
instructors_group: Group of instructors
add_instructeur:
wrong_address:
one: "%{value} is not a valid email address"
other: "%{value} are not valid email addresses"
assignment:
one: "The instructor %{value} was assigned to the group « %{groupe} »."
other: "The instructors %{value} were assigned to the group « %{groupe} »."
reaffecter_dossiers:
existing_groupe:
one: "%{count} group exist"
other: "%{count} groups exist"
instructeurs:
assigned_instructeur:
one: "%{count} instructor is assigned"
other: "%{count} instructors are assigned"
edit:
routing:
title: Routing label
notice: This text will appear on the user form as the label of a list
group_management:
title: Group management
delete: delete the group
delete_confirmation: Are you sure you want to delete the group "%{group_name}"
move_folders: move folders
move_folders_confirmation: Reassign folders to another group so you can delete it
add_a_group:
title: Add a group
notice: This group will be a choice from the list "%{routing_criteria_name}"
placeholder: ex. City of Bordeaux
csv_import:
title: CSV Import
notice_1: The csv file must have 2 columns (Group, Email) and be separated by commas. The import does not overwrite existing groups and instructors.
notice_2: The size of the file must be less than %{csv_max_size}.
download_exemple: Download sample CSV file
import_file: Import file
import_file_procedure_not_published: The import of instructors by CSV file is available once the process has been published
view: view
button:
add_group: Add group
rename: Rename
existing_groupe:
one: "%{count} group exist"
other: "%{count} groups exist"
routing:
title: Routing
notice_html: |
Routing is a feature for procedures requiring the sharing of instructions between different groups according to a specific criterion (territory, theme or other).
<br><br>
This feature makes it possible to route the files to each group, and to no longer need to filter its files among a large quantity of requests. It is therefore particularly suitable for national approaches instructed locally.
<br><br>
Instructors only see the files that concern them, and therefore do not have access to data outside their scope.
self_managment_notice_html: |
Instructor Self-Management allows instructors to self-manage the list of Gait Instructors.
button:
routing_enable: Enable routing
routing_disable: Disable routing

View file

@ -10,6 +10,8 @@ fr:
other: "Les experts %{value} ont été affectés à la démarche n° %{procedure}" other: "Les experts %{value} ont été affectés à la démarche n° %{procedure}"
groupe_instructeurs: groupe_instructeurs:
index: index:
procedures: Démarches
instructors_group: Groupe d'instructeurs
existing_groupe: existing_groupe:
one: "%{count} groupe existe" one: "%{count} groupe existe"
other: "%{count} groupes existent" other: "%{count} groupes existent"
@ -29,10 +31,35 @@ fr:
one: "%{count} instructeur est affecté" one: "%{count} instructeur est affecté"
other: "%{count} instructeurs sont affectés" other: "%{count} instructeurs sont affectés"
edit: edit:
routing:
title: Libellé du routage
notice: Ce texte apparaitra sur le formulaire usager comme le libellé dune liste
group_management:
title: Gestion des Groupes
delete: supprimer le groupe
delete_confirmation: Êtes-vous sûr de vouloir supprimer le groupe "%{group_name}"
move_folders: déplacer les dossiers
move_folders_confirmation: Réaffecter les dossiers à un autre groupe afin de pouvoir le supprimer
add_a_group:
title: Ajouter un groupe
notice: Ce groupe sera un choix de la liste "%{routing_criteria_name}"
placeholder: ex. Ville de Bordeaux
csv_import:
title: Importer par CSV
notice_1: Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
notice_2: Le poids du fichier doit être inférieur à %{csv_max_size}
download_exemple: Télécharger l'exemple de fichier CSV
import_file: Importer le fichier
import_file_procedure_not_published: Limport dinstructeurs par fichier CSV est disponible une fois la démarche publiée
view: voir
button:
add_group: Ajouter le groupe
rename: Renommer
existing_groupe: existing_groupe:
one: "%{count} groupe existe" one: "%{count} groupe existe"
other: "%{count} groupes existent" other: "%{count} groupes existent"
routing: routing:
title: Routage
notice_html: | notice_html: |
Le routage est une fonctionnalité pour les démarches nécessitant le partage de linstruction entre différents groupes en fonction dun critère précis (territoire, thématique ou autre). Le routage est une fonctionnalité pour les démarches nécessitant le partage de linstruction entre différents groupes en fonction dun critère précis (territoire, thématique ou autre).
<br><br> <br><br>
@ -45,3 +72,4 @@ fr:
routing_enable: Activer le routage routing_enable: Activer le routage
routing_disable: Désactiver le routage routing_disable: Désactiver le routage
self_managment_toggle: Activer lautogestion des instructeurs self_managment_toggle: Activer lautogestion des instructeurs
add_group: Ajouter le groupe

View file

@ -0,0 +1,30 @@
en:
instructeurs:
procedures:
email_notifications:
utils:
positive: "Yes"
negative: "No"
title: Email notifications
subtitle: On this page, configure the notifications you wish to receive by email for this procedure.
for_each_file_submitted:
title: Receive a notification for each file submitted
notice_1: This email notifies you that a new file has been submitted.
notice_2: It is sent each time a user submits a file.
for_each_message_submitted:
title: Receive a notification for each message submitted
notice_1: This email notifies you that a new message has been submitted on your following files.
notice_2: It is sent each time a user submits a message.
daily_notifications:
title: Receive a daily notification
notice_1: This email notifies you of the filing of new files on this approach, or of changes to your monitored files.
notice_2: It is sent once a day, Monday to Saturday, around 10 a.m.
hebdo_recap:
title: Receive a weekly recap
notice_1: This email summarizes the activity of the week on all of your procedures.
notice_2: It is sent weekly on Monday morning.
buttons:
back_to_procedure: Back to procedure
save: Save

View file

@ -0,0 +1,28 @@
fr:
instructeurs:
procedures:
email_notifications:
utils:
positive: Oui
negative: Non
title: Notifications par email
subtitle: Configurez sur cette page les notifications que vous souhaitez recevoir par email pour cette démarche.
for_each_file_submitted:
title: Recevoir une notification à chaque dossier déposé
notice_1: Cet email vous signale le dépôt dun nouveau dossier.
notice_2: Il est envoyé à chaque fois qu'un usager dépose un dossier.
for_each_message_submitted:
title: Recevoir une notification à chaque message déposé
notice_1: Cet email vous signale le dépôt dun nouveau message sur vos dossiers suivis.
notice_2: Il est envoyé à chaque fois qu'un usager dépose un message.
daily_notifications:
title: Recevoir une notification quotidienne
notice_1: Cet email vous signale le dépôt de nouveaux dossiers sur cette démarche, ou des changements sur vos dossiers suivis.
notice_2: Il est envoyé une fois par jour, du lundi au samedi, vers 10 h du matin.
hebdo_recap:
title: Recevoir un récapitulatif hebdomadaire
notice_1: Cet email récapitule lactivité de la semaine sur lensemble de vos démarches.
notice_2: Il est envoyé chaque semaine le lundi matin.
buttons:
back_to_procedure: Revenir à la procédure
save: Enregistrer

View file

@ -35,3 +35,8 @@ en:
export_monthly_pending_html: An export of the last 30 days in the format %{export_format} is being generated<br>(asked %{export_time} ago) export_monthly_pending_html: An export of the last 30 days in the format %{export_format} is being generated<br>(asked %{export_time} ago)
download_archive: Download a .zip archive of all files and their attachments download_archive: Download a .zip archive of all files and their attachments
download: Download all files download: Download all files
email_usagers:
contact_users: Contact users (draft)
notice: "You will send a message to %{dossiers_count} whose files are in draft, in the instructor groups : %{groupe_instructeurs}."
stats:
title: Statistics

View file

@ -35,3 +35,8 @@ fr:
export_monthly_pending_html: Un export des 30 derniers jours au format %{export_format} est en train dêtre généré<br>(demandé il y a %{export_time}) export_monthly_pending_html: Un export des 30 derniers jours au format %{export_format} est en train dêtre généré<br>(demandé il y a %{export_time})
download_archive: Télécharger une archive au format .zip de tous les dossiers et leurs pièces jointes download_archive: Télécharger une archive au format .zip de tous les dossiers et leurs pièces jointes
download: Télécharger tous les dossiers download: Télécharger tous les dossiers
email_usagers:
contact_users: Contacter les usagers (brouillon)
notice: "Vous allez envoyer un message à %{dossiers_count} dont les dossiers sont en brouillon, dans les groupes instructeurs : %{groupe_instructeurs}."
stats:
title: Statistiques

View file

@ -1,5 +1,7 @@
en: en:
layouts: layouts:
header:
files: Files
go_superadmin: "Switch to super-admin" go_superadmin: "Switch to super-admin"
go_user: "Switch to user" go_user: "Switch to user"
go_instructor: "Switch to instructor" go_instructor: "Switch to instructor"

View file

@ -1,5 +1,7 @@
fr: fr:
layouts: layouts:
header:
files: Dossiers
go_superadmin: "Passer en super-admin" go_superadmin: "Passer en super-admin"
go_user: "Passer en usager" go_user: "Passer en usager"
go_instructor: "Passer en instructeur" go_instructor: "Passer en instructeur"

View file

@ -4,6 +4,7 @@
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
"@headlessui/react": "^1.5.0", "@headlessui/react": "^1.5.0",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hotwired/stimulus": "^3.0.1",
"@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-draw": "^1.3.0",
"@popperjs/core": "^2.11.4", "@popperjs/core": "^2.11.4",
"@rails/actiontext": "^6.1.4-1", "@rails/actiontext": "^6.1.4-1",

View file

@ -87,7 +87,7 @@ describe 'shared/dossiers/edit.html.haml', type: :view do
let(:champ_value) { ['banana', 'grapefruit'].to_json } let(:champ_value) { ['banana', 'grapefruit'].to_json }
it 'renders the list as a multiple-selection dropdown' do it 'renders the list as a multiple-selection dropdown' do
expect(subject).to have_selector('[data-react-class="ComboMultipleDropdownList"]') expect(subject).to have_selector('[data-react-component-value="ComboMultipleDropdownList"]')
end end
end end
end end

View file

@ -1269,6 +1269,11 @@
resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324"
integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ== integrity sha512-JJCXydOFWMDpCP4q13iEplA503MQO3xLoZiKum+955ZCtHINWnx26CUxVxxFQu/uLb4LW3ge15ZpzIkXKkJ8oQ==
"@hotwired/stimulus@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.0.1.tgz#141f15645acaa3b133b7c247cad58ae252ffae85"
integrity sha512-oHsJhgY2cip+K2ED7vKUNd2P+BEswVhrCYcJ802DSsblJFv7mPFVk3cQKvm2vHgHeDVdnj7oOKrBbzp1u8D+KA==
"@humanwhocodes/config-array@^0.5.0": "@humanwhocodes/config-array@^0.5.0":
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"