Merge branch 'main' into bug-input-pj-apres-suppression

This commit is contained in:
Benoit Queyron 2024-07-10 16:35:07 +02:00
commit ceedbe9c45
No known key found for this signature in database
GPG key ID: 9E723480C7317E22
37 changed files with 221 additions and 368 deletions

View file

@ -10,7 +10,7 @@ class Attachment::EditComponent < ApplicationComponent
EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze EXTENSIONS_ORDER = ['jpeg', 'png', 'pdf', 'zip'].freeze
def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], **kwargs) def initialize(champ: nil, auto_attach_url: nil, attached_file:, direct_upload: true, index: 0, as_multiple: false, view_as: :link, user_can_destroy: true, user_can_replace: false, attachments: [], max: nil, **kwargs)
@champ = champ @champ = champ
@attached_file = attached_file @attached_file = attached_file
@direct_upload = direct_upload @direct_upload = direct_upload
@ -24,6 +24,7 @@ class Attachment::EditComponent < ApplicationComponent
@attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : []) @attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : [])
@attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty? @attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty?
@attachments.compact! @attachments.compact!
@max = max
# Utilisation du premier attachement comme référence pour la rétrocompatibilité # Utilisation du premier attachement comme référence pour la rétrocompatibilité
@attachment = @attachments.first @attachment = @attachments.first
@ -54,7 +55,7 @@ class Attachment::EditComponent < ApplicationComponent
end end
def destroy_attachment_path def destroy_attachment_path
attachment_path(champ_id: champ&.public_id) attachment_path(dossier_id: champ&.dossier_id, stable_id: champ&.stable_id, row_id: champ&.row_id)
end end
def attachment_input_class def attachment_input_class
@ -63,6 +64,7 @@ class Attachment::EditComponent < ApplicationComponent
def file_field_options def file_field_options
track_issue_with_missing_validators if missing_validators? track_issue_with_missing_validators if missing_validators?
options = { options = {
class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?), class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?),
direct_upload: @direct_upload, direct_upload: @direct_upload,
@ -76,6 +78,7 @@ class Attachment::EditComponent < ApplicationComponent
options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {}) options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {})
options[:multiple] = true if as_multiple? options[:multiple] = true if as_multiple?
options[:disabled] = true if @max && @index >= @max
options options
end end

View file

@ -30,10 +30,6 @@ class Attachment::MultipleComponent < ApplicationComponent
@attachments.each_with_index(&block) @attachments.each_with_index(&block)
end end
def can_attach_next?
@attachments.count < @max
end
def empty_component_id def empty_component_id
champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic" champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic"
end end

View file

@ -7,8 +7,8 @@
%li{ id: dom_id(attachment) } %li{ id: dom_id(attachment) }
= render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, view_as:, user_can_destroy:, form_object_name:) = render Attachment::EditComponent.new(champ:, attached_file:, attachment:, index:, view_as:, user_can_destroy:, form_object_name:)
%div{ id: empty_component_id, class: class_names("hidden": !can_attach_next?), data: { turbo_force: :server } } %div{ id: empty_component_id, data: { turbo_force: :server } }
= render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:) = render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:, max: @max)
// single poll and refresh message for all attachments // single poll and refresh message for all attachments
= render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context) = render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context)

View file

@ -2,6 +2,17 @@
class Dsfr::AlertComponent < ApplicationComponent class Dsfr::AlertComponent < ApplicationComponent
renders_one :body renders_one :body
attr_reader :state, :title, :size, :block, :extra_class_names, :heading_level
def initialize(state:, title: '', size: '', extra_class_names: nil, heading_level: 'h3')
@state = state
@title = title
@size = size
@block = block
@extra_class_names = extra_class_names
@heading_level = heading_level
end
def prefix_for_state def prefix_for_state
case state case state
when :error then "Erreur : " when :error then "Erreur : "
@ -19,19 +30,4 @@ class Dsfr::AlertComponent < ApplicationComponent
extra_class_names => true extra_class_names => true
) )
end end
private
def initialize(state:, title: '', size: '', extra_class_names: nil, heading_level: 'h3')
@state = state
@title = title
@size = size
@block = block
@extra_class_names = extra_class_names
@heading_level = heading_level
end
attr_reader :state, :title, :size, :block, :extra_class_names, :heading_level
private
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class ProcedureDraftWarningComponent < ApplicationComponent
attr_reader :revision
attr_reader :current_administrateur
attr_reader :extra_class_names
def initialize(revision:, current_administrateur:, extra_class_names: nil)
@revision = revision
@current_administrateur = current_administrateur
@extra_class_names = extra_class_names
end
def render?
revision.draft?
end
def admin?
current_administrateur.present? && revision.procedure.administrateurs.include?(current_administrateur)
end
end

View file

@ -0,0 +1,13 @@
---
en:
title: Procedure in testing
intro_procedure_brouillon_html: This procedure is currently in <b>testing</b>
intro_revision_draft_html: This page allows you to <b>test</b> a new version of the procedure
body_general_html: |
and this page is reserved for the administration in charge of its deployment.
If you start or submit a file, it may be <b>deleted at any time</b> without notice, even if it is accepted later.
body_user: |
If this link was shared with you, please contact the service in charge of this procedure
to obtain the public link for the procedure in order to submit your application.
body_admin_procedure_brouillon: Do not share this link with your users. When you publish the procedure, you will access the public link for the procedure to be shared.
body_admin_revision_draft: Do not share this link with your users, but rather the public link for the procedure displayed in your administrator dashboard.

View file

@ -0,0 +1,13 @@
---
fr:
title: Démarche en test
intro_procedure_brouillon_html: Cette démarche est actuellement en <b>test</b>
intro_revision_draft_html: Cette page permet de <b>tester</b> une nouvelle version de la démarche
body_general_html: |
et cette page est réservée à ladministration en charge de son déploiement.
Si vous commencez ou déposez un dossier, il pourra être <b>supprimé à tout moment</b> et sans préavis, même après avoir été accepté.
body_user: |
Si ce lien vous a été communiqué, contactez le service en charge de cette démarche
pour obtenir le lien public de la démarche afin de déposer votre dossier.
body_admin_procedure_brouillon: Ne communiquez pas ce lien à vos usagers. Lorsque vous publierez la démarche, vous accéderez au lien public de la démarche à communiquer.
body_admin_revision_draft: Ne communiquez pas ce lien à vos usagers, mais le lien public de la démarche affiché dans votre tableau de bord administrateur.

View file

@ -0,0 +1,10 @@
= render Dsfr::AlertComponent.new(state: :warning, extra_class_names:, title: t(".title")) do |c|
- c.with_body do
%p
= revision.procedure.brouillon? ? t(".intro_procedure_brouillon_html") : t(".intro_revision_draft_html")
= t(".body_general_html")
- if admin?
%p= revision.procedure.brouillon? ? t(".body_admin_procedure_brouillon") : t(".body_admin_revision_draft")
- else
%p= t(".body_user")

View file

@ -12,8 +12,8 @@ class AgentConnect::AgentController < ApplicationController
def login def login
uri, state, nonce = AgentConnectService.authorization_uri uri, state, nonce = AgentConnectService.authorization_uri
cookies.encrypted[STATE_COOKIE_NAME] = state cookies.encrypted[STATE_COOKIE_NAME] = { value: state, secure: Rails.env.production?, httponly: true }
cookies.encrypted[NONCE_COOKIE_NAME] = nonce cookies.encrypted[NONCE_COOKIE_NAME] = { value: nonce, secure: Rails.env.production?, httponly: true }
redirect_to uri, allow_other_host: true redirect_to uri, allow_other_host: true
end end

View file

@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base
def set_locale(locale) def set_locale(locale)
if locale && locale.to_sym.in?(I18n.available_locales) if locale && locale.to_sym.in?(I18n.available_locales)
cookies[:locale] = locale cookies[:locale] = { value: locale, secure: Rails.env.production?, httponly: true }
if user_signed_in? if user_signed_in?
current_user.update(locale: locale) current_user.update(locale: locale)
end end

View file

@ -24,7 +24,8 @@ module ApplicationController::LongLivedAuthenticityToken
cookies.signed[COOKIE_NAME] = { cookies.signed[COOKIE_NAME] = {
value: csrf_token, value: csrf_token,
expires: 1.year.from_now, expires: 1.year.from_now,
httponly: true httponly: true,
secure: Rails.env.production?
} }
session[:_csrf_token] = csrf_token session[:_csrf_token] = csrf_token

View file

@ -21,11 +21,18 @@ class AttachmentsController < ApplicationController
@attachment.purge_later @attachment.purge_later
flash.notice = 'La pièce jointe a bien été supprimée.' flash.notice = 'La pièce jointe a bien été supprimée.'
@champ_id = params[:champ_id] @champ = find_champ if params[:dossier_id]
respond_to do |format| respond_to do |format|
format.turbo_stream format.turbo_stream
format.html { redirect_back(fallback_location: root_url) } format.html { redirect_back(fallback_location: root_url) }
end end
end end
private
def find_champ
dossier = policy_scope(Dossier).includes(:champs).find(params[:dossier_id])
dossier.champs.find_by(stable_id: params[:stable_id], row_id: params[:row_id])
end
end end

View file

@ -247,7 +247,9 @@ module Instructeurs
@export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur) @export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur)
cookies.encrypted[cookies_export_key] = { cookies.encrypted[cookies_export_key] = {
value: DateTime.current, value: DateTime.current,
expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT expires: Export::MAX_DUREE_GENERATION + Export::MAX_DUREE_CONSERVATION_EXPORT,
httponly: true,
secure: Rails.env.production?
} }
respond_to do |format| respond_to do |format|

View file

@ -52,11 +52,14 @@ export function useSingleList({
}: { }: {
defaultItems?: Item[]; defaultItems?: Item[];
defaultSelectedKey?: string | null; defaultSelectedKey?: string | null;
emptyFilterKey?: string; emptyFilterKey?: string | null;
onChange?: (item: Item | null) => void; onChange?: (item: Item | null) => void;
}) { }) {
const [selectedKey, setSelectedKey] = useState(defaultSelectedKey); const [selectedKey, setSelectedKey] = useState(defaultSelectedKey);
const items = useMemo(() => defaultItems || [], [defaultItems]); const items = useMemo(
() => (defaultItems ? distinctBy(defaultItems, 'value') : []),
[defaultItems]
);
const selectedItem = useMemo( const selectedItem = useMemo(
() => items.find((item) => item.value == selectedKey) ?? null, () => items.find((item) => item.value == selectedKey) ?? null,
[items, selectedKey] [items, selectedKey]
@ -82,8 +85,8 @@ export function useSingleList({
const initialSelectedKeyRef = useRef(defaultSelectedKey); const initialSelectedKeyRef = useRef(defaultSelectedKey);
const setSelection = useEvent((key?: string | null) => { const setSelection = useEvent((key?: string | null) => {
const inputValue = defaultSelectedKey const inputValue = key
? items.find((item) => item.value == defaultSelectedKey)?.label ? items.find((item) => item.value == key)?.label
: ''; : '';
setSelectedKey(key); setSelectedKey(key);
setInputValue(inputValue ?? ''); setInputValue(inputValue ?? '');
@ -157,7 +160,10 @@ export function useMultiList({
() => new Set(defaultSelectedKeys ?? []) () => new Set(defaultSelectedKeys ?? [])
); );
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const items = useMemo(() => defaultItems || [], [defaultItems]); const items = useMemo(
() => (defaultItems ? distinctBy(defaultItems, 'value') : []),
[defaultItems]
);
const itemsIndex = useMemo(() => { const itemsIndex = useMemo(() => {
const index = new Map<string, Item>(); const index = new Map<string, Item>();
for (const item of items) { for (const item of items) {
@ -473,3 +479,8 @@ export function useOnFormReset(onReset?: () => void) {
return ref; return ref;
} }
function distinctBy<T>(array: T[], key: keyof T): T[] {
const keys = array.map((item) => item[key]);
return array.filter((item, index) => keys.indexOf(item[key]) == index);
}

View file

@ -46,7 +46,7 @@ export const SingleComboBoxProps = s.assign(
s.partial( s.partial(
s.object({ s.object({
selectedKey: s.nullable(s.string()), selectedKey: s.nullable(s.string()),
emptyFilterKey: s.string() emptyFilterKey: s.nullable(s.string())
}) })
) )
); );

View file

@ -1,4 +1,4 @@
import format from 'date-fns/format'; import { format } from 'date-fns/format';
import { ApplicationController } from './application_controller'; import { ApplicationController } from './application_controller';

View file

@ -1,156 +0,0 @@
import type { AxeResults, NodeResult, RelatedNode } from 'axe-core';
import axe from 'axe-core';
domReady().then(() => {
axe.run(document.body, { reporter: 'v2' }).then((results) => {
logToConsole(results);
});
});
// contrasted against Chrome default color of #ffffff
const lightTheme = {
serious: '#d93251',
minor: '#d24700',
text: 'black'
};
// contrasted against Safari dark mode color of #535353
const darkTheme = {
serious: '#ffb3b3',
minor: '#ffd500',
text: 'white'
};
const theme =
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? darkTheme
: lightTheme;
const boldCourier = 'font-weight:bold;font-family:Courier;';
const critical = `color:${theme.serious};font-weight:bold;`;
const serious = `color:${theme.serious};font-weight:normal;`;
const moderate = `color:${theme.minor};font-weight:bold;`;
const minor = `color:${theme.minor};font-weight:normal;`;
const defaultReset = `font-color:${theme.text};font-weight:normal;`;
function logToConsole(results: AxeResults): void {
console.group('%cNew axe issues', serious);
results.violations.forEach((result) => {
let fmt: string;
switch (result.impact) {
case 'critical':
fmt = critical;
break;
case 'serious':
fmt = serious;
break;
case 'moderate':
fmt = moderate;
break;
case 'minor':
fmt = minor;
break;
default:
fmt = minor;
break;
}
console.groupCollapsed(
'%c%s: %c%s %s',
fmt,
result.impact,
defaultReset,
result.help,
result.helpUrl
);
result.nodes.forEach((node) => {
failureSummary(node, 'any');
failureSummary(node, 'none');
});
console.groupEnd();
});
console.groupEnd();
}
function failureSummary(node: NodeResult, key: AxeCoreNodeResultKey): void {
if (node[key].length > 0) {
logElement(node, console.groupCollapsed);
logHtml(node);
logFailureMessage(node, key);
let relatedNodes: RelatedNode[] = [];
node[key].forEach((check) => {
relatedNodes = relatedNodes.concat(check.relatedNodes ?? []);
});
if (relatedNodes.length > 0) {
console.groupCollapsed('Related nodes');
relatedNodes.forEach((relatedNode) => {
logElement(relatedNode, console.log);
logHtml(relatedNode);
});
console.groupEnd();
}
console.groupEnd();
}
}
function logFailureMessage(node: NodeResult, key: AxeCoreNodeResultKey): void {
// this exists on axe but we don't export it as part of the typescript
// namespace, so just let me use it as I need
const message: string = (
axe as unknown as AxeWithAudit
)._audit.data.failureSummaries[key].failureMessage(
node[key].map((check) => check.message || '')
);
console.error(message);
}
function logElement(
node: NodeResult | RelatedNode,
logFn: (...args: unknown[]) => void
): void {
const el = document.querySelector(node.target.toString());
if (!el) {
logFn('Selector: %c%s', boldCourier, node.target.toString());
} else {
logFn('Element: %o', el);
}
}
function logHtml(node: NodeResult | RelatedNode): void {
console.log('HTML: %c%s', boldCourier, node.html);
}
type AxeCoreNodeResultKey = 'any' | 'all' | 'none';
interface AxeWithAudit {
_audit: {
data: {
failureSummaries: {
any: {
failureMessage: (args: string[]) => string;
};
all: {
failureMessage: (args: string[]) => string;
};
none: {
failureMessage: (args: string[]) => string;
};
};
};
};
}
function domReady() {
return new Promise<void>((resolve) => {
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', () => resolve(), {
once: true
});
} else {
resolve();
}
});
}

View file

@ -1,75 +0,0 @@
/*
@preserve dataset polyfill for IE < 11. See https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset and http://caniuse.com/#search=dataset
@author ShirtlessKirk copyright 2015
@license WTFPL (http://www.wtfpl.net/txt/copying)
*/
const dash = /-([a-z])/gi;
const dataRegEx = /^data-(.+)/;
const hasEventListener = !!document.addEventListener;
const test = document.createElement('_');
const DOMAttrModified = 'DOMAttrModified';
let mutationSupport = false;
function clearDataset(event) {
delete event.target._datasetCache;
}
function toCamelCase(string) {
return string.replace(dash, function (_, letter) {
return letter.toUpperCase();
});
}
function getDataset() {
const dataset = {};
for (let attribute of this.attributes) {
let match = attribute.name.match(dataRegEx);
if (match) {
dataset[toCamelCase(match[1])] = attribute.value;
}
}
return dataset;
}
function mutation() {
if (hasEventListener) {
test.removeEventListener(DOMAttrModified, mutation, false);
} else {
test.detachEvent(`on${DOMAttrModified}`, mutation);
}
mutationSupport = true;
}
if (!test.dataset) {
if (hasEventListener) {
test.addEventListener(DOMAttrModified, mutation, false);
} else {
test.attachEvent(`on${DOMAttrModified}`, mutation);
}
// trigger event (if supported)
test.setAttribute('foo', 'bar');
Object.defineProperty(Element.prototype, 'dataset', {
get: mutationSupport
? function get() {
if (!this._datasetCache) {
this._datasetCache = getDataset.call(this);
}
return this._datasetCache;
}
: getDataset
});
if (mutationSupport && hasEventListener) {
// < IE9 supports neither
document.addEventListener(DOMAttrModified, clearDataset, false);
}
}

View file

@ -107,11 +107,6 @@ class Champ < ApplicationRecord
[to_s] [to_s]
end end
def valid_value
return unless valid_champ_value?
value
end
def to_s def to_s
TypeDeChamp.champ_value(type_champ, self) TypeDeChamp.champ_value(type_champ, self)
end end

View file

@ -50,7 +50,7 @@ class Champs::COJOChamp < Champ
def update_external_id def update_external_id
if accreditation_number_changed? || accreditation_birthdate_changed? if accreditation_number_changed? || accreditation_birthdate_changed?
if accreditation_number.present? && accreditation_birthdate.present? && /\A\d+\z/.match?(accreditation_number) if accreditation_number.present? && accreditation_birthdate.present? && /\A[\d-]+\z/.match?(accreditation_number)
self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json self.external_id = { accreditation_number:, accreditation_birthdate: }.to_json
else else
self.external_id = nil self.external_id = nil

View file

@ -8,7 +8,8 @@ module TrustedDeviceConcern
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = { cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
value: JSON.generate({ created_at: start_at }), value: JSON.generate({ created_at: start_at }),
expires: start_at + TRUSTED_DEVICE_PERIOD, expires: start_at + TRUSTED_DEVICE_PERIOD,
httponly: true httponly: true,
secure: Rails.env.production?
} }
end end

View file

@ -20,7 +20,7 @@ class TypesDeChamp::DecimalNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
private private
def champ_formatted_value(champ) def champ_formatted_value(champ)
champ.valid_value&.to_f champ.value&.to_f
end end
end end
end end

View file

@ -20,7 +20,7 @@ class TypesDeChamp::IntegerNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
private private
def champ_formatted_value(champ) def champ_formatted_value(champ)
champ.valid_value&.to_i champ.value&.to_i
end end
end end
end end

View file

@ -66,12 +66,12 @@ class TypesDeChamp::TypeDeChampBase
when 2 when 2
champ_value(champ) champ_value(champ)
else else
champ.valid_value.presence || champ_default_api_value(version) champ.value.presence || champ_default_api_value(version)
end end
end end
def champ_value_for_export(champ, path = :value) def champ_value_for_export(champ, path = :value)
path == :value ? champ.valid_value.presence : champ_default_export_value(path) path == :value ? champ.value.presence : champ_default_export_value(path)
end end
def champ_value_for_tag(champ, path = :value) def champ_value_for_tag(champ, path = :value)

View file

@ -1,7 +1,9 @@
= turbo_stream.remove dom_id(@attachment, :persisted_row) = turbo_stream.remove dom_id(@attachment, :persisted_row)
- if @champ_id
= turbo_stream.show "attachment-multiple-empty-#{@champ_id}"
= turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input"
= turbo_stream.show_all ".attachment-input-#{@attachment.id}" = turbo_stream.show_all ".attachment-input-#{@attachment.id}"
- if @champ
= fields_for @champ.input_name, @champ do |form|
= turbo_stream.replace @champ.input_group_id do
= render EditableChamp::EditableChampComponent.new champ: @champ, form: form
= turbo_stream.focus_all "#attachment-multiple-empty-#{@champ.public_id} input"

View file

@ -13,7 +13,11 @@
#{Current.application_name} #{Current.application_name}
%li= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--secondary' %li= link_to t('views.shared.account.already_user'), commencer_sign_in_path(path: @procedure.path, prefill_token: @prefilled_dossier&.prefill_token), class: 'fr-btn fr-btn--secondary'
= render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
- else - else
= render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
- if @prefilled_dossier - if @prefilled_dossier
= render Dsfr::CalloutComponent.new(title: t(".prefilled_draft"), heading_level: 'h2') do |c| = render Dsfr::CalloutComponent.new(title: t(".prefilled_draft"), heading_level: 'h2') do |c|
- c.with_body do - c.with_body do

View file

@ -2,4 +2,9 @@
= render partial: 'header', locals: { avis: @avis, dossier: @dossier } = render partial: 'header', locals: { avis: @avis, dossier: @dossier }
= render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'expert' } .fr-container
.fr-grid-row.fr-grid-row--center
- summary = ViewableChamp::HeaderSectionsSummaryComponent.new(dossier: @dossier, is_private: false)
= render summary
%div{ class: class_names("fr-col-12", "fr-col-xl-9" => summary.render?, "fr-col-xl-8" => !summary.render?) }
= render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: nil, profile: 'expert' }

View file

@ -6,20 +6,15 @@
.dossier-container.fr-mb-4w .dossier-container.fr-mb-4w
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
- if @dossier.en_construction?
.fr-container
.fr-grid-row.fr-grid-row--center
.fr-col-xl-10
= render Dossiers::EnConstructionNotSubmittedComponent.new(dossier: @dossier, user: current_user)
.fr-container .fr-container
.fr-grid-row.fr-grid-row--center .fr-grid-row.fr-grid-row--center
.fr-col-md-9 .fr-col-md-9
- if @dossier.en_construction?
= render Dossiers::EnConstructionNotSubmittedComponent.new(dossier: @dossier, user: current_user)
= render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' } = render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' }
- if !@dossier.read_only?
- if !@dossier.read_only? .fr-px-2w.fr-mt-2w
.fr-container.fr-mt-2w %p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm',
.fr-grid-row.fr-grid-row--center title: t('views.users.dossiers.demande.edit_dossier_title')
.fr-col-xl-8.fr-col-offset-xl-2
%p= link_to t('views.users.dossiers.demande.edit_dossier'), modifier_dossier_path(@dossier), class: 'fr-btn fr-btn-sm',
title: t('views.users.dossiers.demande.edit_dossier_title')

BIN
bun.lockb

Binary file not shown.

View file

@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_DS_session' Rails.application.config.session_store :cookie_store, key: '_DS_session', secure: Rails.env.production?, httponly: true

View file

@ -10,7 +10,7 @@ fr:
since: "depuis le %{date}" since: "depuis le %{date}"
closed: "Close" closed: "Close"
published: "Publiée" published: "Publiée"
draft: "En test" draft: "En test"
more_info_on_test: "Pour plus dinformation sur la phase de test" more_info_on_test: "Pour plus dinformation sur la phase de test"
go_to_FAQ: "consulter la FAQ" go_to_FAQ: "consulter la FAQ"
url_FAQ: "/faq#accordion-administrateur-2" url_FAQ: "/faq#accordion-administrateur-2"

View file

@ -5,9 +5,9 @@
"@coldwired/react": "^0.15.0", "@coldwired/react": "^0.15.0",
"@coldwired/turbo-stream": "^0.13.0", "@coldwired/turbo-stream": "^0.13.0",
"@coldwired/utils": "^0.13.0", "@coldwired/utils": "^0.13.0",
"@frsource/autoresize-textarea": "^2.0.75", "@frsource/autoresize-textarea": "^2.0.82",
"@gouvfr/dsfr": "^1.11.2", "@gouvfr/dsfr": "^1.11.2",
"@graphiql/plugin-explorer": "^3.0.2", "@graphiql/plugin-explorer": "^3.1.0",
"@graphiql/toolkit": "^0.9.1", "@graphiql/toolkit": "^0.9.1",
"@headlessui/react": "^1.6.6", "@headlessui/react": "^1.6.6",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
@ -15,11 +15,11 @@
"@hotwired/turbo": "^7.3.0", "@hotwired/turbo": "^7.3.0",
"@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-draw": "^1.3.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@rails/actiontext": "^7.1.3-2", "@rails/actiontext": "^7.1.3-4",
"@rails/activestorage": "^7.1.3-2", "@rails/activestorage": "^7.1.3-4",
"@rails/ujs": "^7.1.3-2", "@rails/ujs": "^7.1.3-4",
"@reach/slider": "^0.17.0", "@reach/slider": "^0.17.0",
"@sentry/browser": "8.7.0", "@sentry/browser": "8.15.0",
"@tiptap/core": "^2.2.4", "@tiptap/core": "^2.2.4",
"@tiptap/extension-bold": "^2.2.4", "@tiptap/extension-bold": "^2.2.4",
"@tiptap/extension-bullet-list": "^2.2.4", "@tiptap/extension-bullet-list": "^2.2.4",
@ -44,26 +44,26 @@
"@tmcw/togeojson": "^5.6.0", "@tmcw/togeojson": "^5.6.0",
"chartkick": "^5.0.1", "chartkick": "^5.0.1",
"core-js": "^3.37.1", "core-js": "^3.37.1",
"date-fns": "^2.30.0", "date-fns": "^3.6.0",
"debounce": "^1.2.1", "debounce": "^2.1.0",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"graphiql": "^3.2.3", "graphiql": "^3.3.2",
"graphql": "^16.8.1", "graphql": "^16.9.0",
"highcharts": "^10.3.3", "highcharts": "^10.3.3",
"lightgallery": "^2.7.2", "lightgallery": "^2.7.2",
"maplibre-gl": "^1.15.2", "maplibre-gl": "^1.15.2",
"match-sorter": "^6.3.4", "match-sorter": "^6.3.4",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "^18.3.0", "react": "^18.3.1",
"react-aria-components": "^1.2.0", "react-aria-components": "^1.2.1",
"react-coordinate-input": "^1.0.0", "react-coordinate-input": "^1.0.0",
"react-dom": "^18.3.0", "react-dom": "^18.3.1",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"react-use-event-hook": "^0.9.6", "react-use-event-hook": "^0.9.6",
"spectaql": "^2.3.1", "spectaql": "^2.3.1",
"stimulus-use": "^0.52.2", "stimulus-use": "^0.52.2",
"superstruct": "^1.0.4", "superstruct": "^2.0.2",
"terser": "^5.31.0", "terser": "^5.31.1",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"trix": "^1.2.3", "trix": "^1.2.3",
@ -73,7 +73,7 @@
"@esbuild/darwin-arm64": "=0.19.9", "@esbuild/darwin-arm64": "=0.19.9",
"@esbuild/linux-x64": "=0.19.9", "@esbuild/linux-x64": "=0.19.9",
"@esbuild/win32-x64": "=0.19.9", "@esbuild/win32-x64": "=0.19.9",
"@react-aria/optimize-locales-plugin": "^1.1.0", "@react-aria/optimize-locales-plugin": "^1.1.1",
"@rollup/rollup-darwin-arm64": "=4.9.1", "@rollup/rollup-darwin-arm64": "=4.9.1",
"@rollup/rollup-linux-x64-gnu": "=4.9.1", "@rollup/rollup-linux-x64-gnu": "=4.9.1",
"@rollup/rollup-win32-x64-msvc": "=4.9.1", "@rollup/rollup-win32-x64-msvc": "=4.9.1",
@ -83,28 +83,27 @@
"@types/mapbox__mapbox-gl-draw": "^1.2.5", "@types/mapbox__mapbox-gl-draw": "^1.2.5",
"@types/rails__activestorage": "^7.1.1", "@types/rails__activestorage": "^7.1.1",
"@types/rails__ujs": "^6.0.4", "@types/rails__ujs": "^6.0.4",
"@types/react": "^18.2.79", "@types/react": "^18.3.3",
"@types/react-dom": "^18.2.25", "@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.11.0", "@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.11.0", "@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"axe-core": "^4.8.4",
"del-cli": "^5.1.0", "del-cli": "^5.1.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.34.2", "eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"jsdom": "^22.1.0", "jsdom": "^24.1.0",
"postcss": "^8.4.38", "postcss": "^8.4.39",
"prettier": "^3.3.0", "prettier": "^3.3.2",
"typescript": "^5.4.5", "typescript": "^5.5.3",
"vite": "^5.2.12", "vite": "^5.3.3",
"vite-plugin-full-reload": "^1.1.0", "vite-plugin-full-reload": "^1.1.0",
"vite-plugin-ruby": "^5.0.0", "vite-plugin-ruby": "^5.0.0",
"vitest": "^1.6.0" "vitest": "^2.0.0"
}, },
"scripts": { "scripts": {
"clean": "del tmp public/graphql && bin/vite clobber", "clean": "del tmp public/graphql && bin/vite clobber",

View file

@ -75,8 +75,8 @@ RSpec.describe Attachment::MultipleComponent, type: :component do
context 'max attachments' do context 'max attachments' do
let(:kwargs) { { max: 1 } } let(:kwargs) { { max: 1 } }
it 'does not render visible input file where max attachments has been reached' do it 'renders a disabled input file where max attachments has been reached' do
expect(subject).to have_selector('.hidden input[type=file]') expect(subject).to have_selector('input[type=file][disabled]')
end end
end end

View file

@ -40,12 +40,6 @@ describe Logic::ChampValue do
it { is_expected.to be nil } it { is_expected.to be nil }
end end
context 'with invalid value' do
before { champ.value = 'environ 300' }
it { is_expected.to be nil }
end
end end
context 'decimal tdc' do context 'decimal tdc' do
@ -53,18 +47,6 @@ describe Logic::ChampValue do
it { expect(champ_value(champ.stable_id).type([champ.type_de_champ])).to eq(:number) } it { expect(champ_value(champ.stable_id).type([champ.type_de_champ])).to eq(:number) }
it { is_expected.to eq(42.01) } it { is_expected.to eq(42.01) }
context 'with invalid value with too many digits after the decimal point' do
before { champ.value = '42.1234' }
it { is_expected.to be nil }
end
context 'with invalid value' do
before { champ.value = 'racine de 2' }
it { is_expected.to be nil }
end
end end
context 'dropdown tdc' do context 'dropdown tdc' do

View file

@ -16,17 +16,11 @@ describe 'administrateurs/procedures/show', type: :view do
render render
end end
describe 'publish button is visible' do it "render content" do
it { expect(rendered).to have_css('#publish-procedure-link') } expect(rendered).to have_css('#publish-procedure-link')
it { expect(rendered).not_to have_css('#close-procedure-link') } expect(rendered).not_to have_css('#close-procedure-link')
end expect(rendered).to have_content('En test')
expect(rendered).not_to have_css('#archive-procedure')
describe 'procedure path is not customized' do
it { expect(rendered).to have_content('En test') }
end
describe 'archive button' do
it { expect(rendered).not_to have_css('#archive-procedure') }
end end
end end
end end

View file

@ -7,10 +7,15 @@ RSpec.describe 'commencer/show', type: :view do
let(:drafts) { [] } let(:drafts) { [] }
let(:not_drafts) { [] } let(:not_drafts) { [] }
let(:preview_dossiers) { dossiers.take(3) } let(:preview_dossiers) { dossiers.take(3) }
let(:user) { nil }
before do
allow(view).to receive(:current_administrateur).and_return(user&.administrateur)
end
before do before do
assign(:procedure, procedure) assign(:procedure, procedure)
assign(:revision, procedure.published_revision) assign(:revision, procedure.active_revision)
assign(:dossiers, dossiers) assign(:dossiers, dossiers)
assign(:drafts, drafts) assign(:drafts, drafts)
assign(:not_drafts, not_drafts) assign(:not_drafts, not_drafts)
@ -25,8 +30,6 @@ RSpec.describe 'commencer/show', type: :view do
subject { render } subject { render }
context 'when no user is signed in' do context 'when no user is signed in' do
let(:user) { nil }
it 'renders sign-in and sign-up links' do it 'renders sign-in and sign-up links' do
subject subject
expect(rendered).to have_link('Créer un compte') expect(rendered).to have_link('Créer un compte')
@ -98,4 +101,35 @@ RSpec.describe 'commencer/show', type: :view do
end end
end end
end end
context "procedure is draft" do
let(:procedure) { create(:procedure, :draft) }
let(:user) { create :user }
it 'renders a warning' do
subject
expect(rendered).to have_text("Cette démarche est actuellement en test")
end
context "when user is admin" do
let(:user) { procedure.administrateurs.first.user }
it "renders warning about draft" do
subject
expect(rendered).to have_text("Cette démarche est actuellement en test")
expect(rendered).to have_text("Ne communiquez pas ce lien")
end
end
end
context "revision is draft" do
before {
assign(:revision, procedure.draft_revision)
}
it "renders warning about draft" do
subject
expect(rendered).to have_text("Démarche en test")
end
end
end end

View file

@ -2,7 +2,7 @@ import { defineConfig } from 'vite';
import ViteReact from '@vitejs/plugin-react'; import ViteReact from '@vitejs/plugin-react';
import RubyPlugin from 'vite-plugin-ruby'; import RubyPlugin from 'vite-plugin-ruby';
import FullReload from 'vite-plugin-full-reload'; import FullReload from 'vite-plugin-full-reload';
import optimizeLocales from '@react-aria/optimize-locales-plugin'; //import optimizeLocales from '@react-aria/optimize-locales-plugin';
const plugins = [ const plugins = [
RubyPlugin(), RubyPlugin(),
@ -10,13 +10,13 @@ const plugins = [
FullReload( FullReload(
['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'], ['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'],
{ delay: 200 } { delay: 200 }
), )
{ // {
...optimizeLocales.vite({ // ...optimizeLocales.vite({
locales: ['en-GB', 'fr-FR'] // locales: ['en-GB', 'fr-FR']
}), // }),
enforce: 'pre' as const // enforce: 'pre' as const
} // }
]; ];
export default defineConfig({ export default defineConfig({