diff --git a/app/components/attachment/edit_component.rb b/app/components/attachment/edit_component.rb
index cd2e7628c..96a5a8ea4 100644
--- a/app/components/attachment/edit_component.rb
+++ b/app/components/attachment/edit_component.rb
@@ -10,7 +10,7 @@ class Attachment::EditComponent < ApplicationComponent
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
@attached_file = attached_file
@direct_upload = direct_upload
@@ -24,6 +24,7 @@ class Attachment::EditComponent < ApplicationComponent
@attachments = attachments.presence || (kwargs.key?(:attachment) ? [kwargs.delete(:attachment)] : [])
@attachments << attached_file.attachment if attached_file.respond_to?(:attachment) && @attachments.empty?
@attachments.compact!
+ @max = max
# Utilisation du premier attachement comme référence pour la rétrocompatibilité
@attachment = @attachments.first
@@ -54,7 +55,7 @@ class Attachment::EditComponent < ApplicationComponent
end
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
def attachment_input_class
@@ -63,6 +64,7 @@ class Attachment::EditComponent < ApplicationComponent
def file_field_options
track_issue_with_missing_validators if missing_validators?
+
options = {
class: class_names("fr-upload attachment-input": true, "#{attachment_input_class}": true, "hidden": persisted?),
direct_upload: @direct_upload,
@@ -76,6 +78,7 @@ class Attachment::EditComponent < ApplicationComponent
options.merge!(has_content_type_validator? ? { accept: accept_content_type } : {})
options[:multiple] = true if as_multiple?
+ options[:disabled] = true if @max && @index >= @max
options
end
diff --git a/app/components/attachment/multiple_component.rb b/app/components/attachment/multiple_component.rb
index 80d2fd941..f994ed1cd 100644
--- a/app/components/attachment/multiple_component.rb
+++ b/app/components/attachment/multiple_component.rb
@@ -30,10 +30,6 @@ class Attachment::MultipleComponent < ApplicationComponent
@attachments.each_with_index(&block)
end
- def can_attach_next?
- @attachments.count < @max
- end
-
def empty_component_id
champ.present? ? "attachment-multiple-empty-#{champ.public_id}" : "attachment-multiple-empty-generic"
end
diff --git a/app/components/attachment/multiple_component/multiple_component.html.haml b/app/components/attachment/multiple_component/multiple_component.html.haml
index 74abdf23c..40b94577f 100644
--- a/app/components/attachment/multiple_component/multiple_component.html.haml
+++ b/app/components/attachment/multiple_component/multiple_component.html.haml
@@ -7,8 +7,8 @@
%li{ id: dom_id(attachment) }
= 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 } }
- = render Attachment::EditComponent.new(champ:, as_multiple: champ.nil?, attached_file:, attachment: nil, index: attachments_count, user_can_destroy:, form_object_name:)
+ %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:, max: @max)
// single poll and refresh message for all attachments
= render Attachment::PendingPollComponent.new(attachments: attachments, poll_url:, context: poll_context)
diff --git a/app/components/dsfr/alert_component.rb b/app/components/dsfr/alert_component.rb
index 7323f639b..dd18f4a14 100644
--- a/app/components/dsfr/alert_component.rb
+++ b/app/components/dsfr/alert_component.rb
@@ -2,6 +2,17 @@
class Dsfr::AlertComponent < ApplicationComponent
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
case state
when :error then "Erreur : "
@@ -19,19 +30,4 @@ class Dsfr::AlertComponent < ApplicationComponent
extra_class_names => true
)
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
diff --git a/app/components/procedure_draft_warning_component.rb b/app/components/procedure_draft_warning_component.rb
new file mode 100644
index 000000000..10c409600
--- /dev/null
+++ b/app/components/procedure_draft_warning_component.rb
@@ -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
diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml
new file mode 100644
index 000000000..6f8dc61bb
--- /dev/null
+++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.en.yml
@@ -0,0 +1,13 @@
+---
+en:
+ title: Procedure in testing
+ intro_procedure_brouillon_html: This procedure is currently in testing
+ intro_revision_draft_html: This page allows you to test 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 deleted at any time 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.
diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml
new file mode 100644
index 000000000..5b0bc7f0f
--- /dev/null
+++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.fr.yml
@@ -0,0 +1,13 @@
+---
+fr:
+ title: Démarche en test
+ intro_procedure_brouillon_html: Cette démarche est actuellement en test
+ intro_revision_draft_html: Cette page permet de tester une nouvelle version de la démarche
+ body_general_html: |
+ et cette page est réservée à l’administration en charge de son déploiement.
+ Si vous commencez ou déposez un dossier, il pourra être supprimé à tout moment 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.
diff --git a/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml
new file mode 100644
index 000000000..929b1e378
--- /dev/null
+++ b/app/components/procedure_draft_warning_component/procedure_draft_warning_component.html.haml
@@ -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")
diff --git a/app/controllers/agent_connect/agent_controller.rb b/app/controllers/agent_connect/agent_controller.rb
index 6116b1f2b..c8ab572e2 100644
--- a/app/controllers/agent_connect/agent_controller.rb
+++ b/app/controllers/agent_connect/agent_controller.rb
@@ -12,8 +12,8 @@ class AgentConnect::AgentController < ApplicationController
def login
uri, state, nonce = AgentConnectService.authorization_uri
- cookies.encrypted[STATE_COOKIE_NAME] = state
- cookies.encrypted[NONCE_COOKIE_NAME] = nonce
+ cookies.encrypted[STATE_COOKIE_NAME] = { value: state, secure: Rails.env.production?, httponly: true }
+ cookies.encrypted[NONCE_COOKIE_NAME] = { value: nonce, secure: Rails.env.production?, httponly: true }
redirect_to uri, allow_other_host: true
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bc172fe47..659b44a31 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base
def set_locale(locale)
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?
current_user.update(locale: locale)
end
diff --git a/app/controllers/application_controller/long_lived_authenticity_token.rb b/app/controllers/application_controller/long_lived_authenticity_token.rb
index cb10c52bd..54eb16f31 100644
--- a/app/controllers/application_controller/long_lived_authenticity_token.rb
+++ b/app/controllers/application_controller/long_lived_authenticity_token.rb
@@ -24,7 +24,8 @@ module ApplicationController::LongLivedAuthenticityToken
cookies.signed[COOKIE_NAME] = {
value: csrf_token,
expires: 1.year.from_now,
- httponly: true
+ httponly: true,
+ secure: Rails.env.production?
}
session[:_csrf_token] = csrf_token
diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb
index 410c64212..721ec2cc2 100644
--- a/app/controllers/attachments_controller.rb
+++ b/app/controllers/attachments_controller.rb
@@ -21,11 +21,18 @@ class AttachmentsController < ApplicationController
@attachment.purge_later
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|
format.turbo_stream
format.html { redirect_back(fallback_location: root_url) }
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
diff --git a/app/controllers/instructeurs/procedures_controller.rb b/app/controllers/instructeurs/procedures_controller.rb
index f737ea591..c61b19aa4 100644
--- a/app/controllers/instructeurs/procedures_controller.rb
+++ b/app/controllers/instructeurs/procedures_controller.rb
@@ -247,7 +247,9 @@ module Instructeurs
@export_templates = current_instructeur.export_templates_for(@procedure).includes(:groupe_instructeur)
cookies.encrypted[cookies_export_key] = {
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|
diff --git a/app/javascript/components/react-aria/hooks.ts b/app/javascript/components/react-aria/hooks.ts
index e2683b919..e00bff289 100644
--- a/app/javascript/components/react-aria/hooks.ts
+++ b/app/javascript/components/react-aria/hooks.ts
@@ -52,11 +52,14 @@ export function useSingleList({
}: {
defaultItems?: Item[];
defaultSelectedKey?: string | null;
- emptyFilterKey?: string;
+ emptyFilterKey?: string | null;
onChange?: (item: Item | null) => void;
}) {
const [selectedKey, setSelectedKey] = useState(defaultSelectedKey);
- const items = useMemo(() => defaultItems || [], [defaultItems]);
+ const items = useMemo(
+ () => (defaultItems ? distinctBy(defaultItems, 'value') : []),
+ [defaultItems]
+ );
const selectedItem = useMemo(
() => items.find((item) => item.value == selectedKey) ?? null,
[items, selectedKey]
@@ -82,8 +85,8 @@ export function useSingleList({
const initialSelectedKeyRef = useRef(defaultSelectedKey);
const setSelection = useEvent((key?: string | null) => {
- const inputValue = defaultSelectedKey
- ? items.find((item) => item.value == defaultSelectedKey)?.label
+ const inputValue = key
+ ? items.find((item) => item.value == key)?.label
: '';
setSelectedKey(key);
setInputValue(inputValue ?? '');
@@ -157,7 +160,10 @@ export function useMultiList({
() => new Set(defaultSelectedKeys ?? [])
);
const [inputValue, setInputValue] = useState('');
- const items = useMemo(() => defaultItems || [], [defaultItems]);
+ const items = useMemo(
+ () => (defaultItems ? distinctBy(defaultItems, 'value') : []),
+ [defaultItems]
+ );
const itemsIndex = useMemo(() => {
const index = new Map();
for (const item of items) {
@@ -473,3 +479,8 @@ export function useOnFormReset(onReset?: () => void) {
return ref;
}
+
+function distinctBy(array: T[], key: keyof T): T[] {
+ const keys = array.map((item) => item[key]);
+ return array.filter((item, index) => keys.indexOf(item[key]) == index);
+}
diff --git a/app/javascript/components/react-aria/props.ts b/app/javascript/components/react-aria/props.ts
index 835b086ea..4932551c5 100644
--- a/app/javascript/components/react-aria/props.ts
+++ b/app/javascript/components/react-aria/props.ts
@@ -46,7 +46,7 @@ export const SingleComboBoxProps = s.assign(
s.partial(
s.object({
selectedKey: s.nullable(s.string()),
- emptyFilterKey: s.string()
+ emptyFilterKey: s.nullable(s.string())
})
)
);
diff --git a/app/javascript/controllers/datetime_controller.ts b/app/javascript/controllers/datetime_controller.ts
index 0567167d0..58dcb1e98 100644
--- a/app/javascript/controllers/datetime_controller.ts
+++ b/app/javascript/controllers/datetime_controller.ts
@@ -1,4 +1,4 @@
-import format from 'date-fns/format';
+import { format } from 'date-fns/format';
import { ApplicationController } from './application_controller';
diff --git a/app/javascript/entrypoints/axe-core.ts b/app/javascript/entrypoints/axe-core.ts
deleted file mode 100644
index 4ca470d6e..000000000
--- a/app/javascript/entrypoints/axe-core.ts
+++ /dev/null
@@ -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((resolve) => {
- if (document.readyState == 'loading') {
- document.addEventListener('DOMContentLoaded', () => resolve(), {
- once: true
- });
- } else {
- resolve();
- }
- });
-}
diff --git a/app/javascript/shared/polyfills/dataset.js b/app/javascript/shared/polyfills/dataset.js
deleted file mode 100644
index 653787247..000000000
--- a/app/javascript/shared/polyfills/dataset.js
+++ /dev/null
@@ -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);
- }
-}
diff --git a/app/models/champ.rb b/app/models/champ.rb
index c144fb470..0f3eb9525 100644
--- a/app/models/champ.rb
+++ b/app/models/champ.rb
@@ -107,11 +107,6 @@ class Champ < ApplicationRecord
[to_s]
end
- def valid_value
- return unless valid_champ_value?
- value
- end
-
def to_s
TypeDeChamp.champ_value(type_champ, self)
end
diff --git a/app/models/champs/cojo_champ.rb b/app/models/champs/cojo_champ.rb
index 514db2a8e..3a356e838 100644
--- a/app/models/champs/cojo_champ.rb
+++ b/app/models/champs/cojo_champ.rb
@@ -50,7 +50,7 @@ class Champs::COJOChamp < Champ
def update_external_id
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
else
self.external_id = nil
diff --git a/app/models/concerns/trusted_device_concern.rb b/app/models/concerns/trusted_device_concern.rb
index 2aa895893..1765f565c 100644
--- a/app/models/concerns/trusted_device_concern.rb
+++ b/app/models/concerns/trusted_device_concern.rb
@@ -8,7 +8,8 @@ module TrustedDeviceConcern
cookies.encrypted[TRUSTED_DEVICE_COOKIE_NAME] = {
value: JSON.generate({ created_at: start_at }),
expires: start_at + TRUSTED_DEVICE_PERIOD,
- httponly: true
+ httponly: true,
+ secure: Rails.env.production?
}
end
diff --git a/app/models/types_de_champ/decimal_number_type_de_champ.rb b/app/models/types_de_champ/decimal_number_type_de_champ.rb
index 9455283f5..486c4e5c8 100644
--- a/app/models/types_de_champ/decimal_number_type_de_champ.rb
+++ b/app/models/types_de_champ/decimal_number_type_de_champ.rb
@@ -20,7 +20,7 @@ class TypesDeChamp::DecimalNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
private
def champ_formatted_value(champ)
- champ.valid_value&.to_f
+ champ.value&.to_f
end
end
end
diff --git a/app/models/types_de_champ/integer_number_type_de_champ.rb b/app/models/types_de_champ/integer_number_type_de_champ.rb
index 7c2d3ef58..515f475aa 100644
--- a/app/models/types_de_champ/integer_number_type_de_champ.rb
+++ b/app/models/types_de_champ/integer_number_type_de_champ.rb
@@ -20,7 +20,7 @@ class TypesDeChamp::IntegerNumberTypeDeChamp < TypesDeChamp::TypeDeChampBase
private
def champ_formatted_value(champ)
- champ.valid_value&.to_i
+ champ.value&.to_i
end
end
end
diff --git a/app/models/types_de_champ/type_de_champ_base.rb b/app/models/types_de_champ/type_de_champ_base.rb
index 81217caa2..02570c8d1 100644
--- a/app/models/types_de_champ/type_de_champ_base.rb
+++ b/app/models/types_de_champ/type_de_champ_base.rb
@@ -66,12 +66,12 @@ class TypesDeChamp::TypeDeChampBase
when 2
champ_value(champ)
else
- champ.valid_value.presence || champ_default_api_value(version)
+ champ.value.presence || champ_default_api_value(version)
end
end
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
def champ_value_for_tag(champ, path = :value)
diff --git a/app/views/attachments/destroy.turbo_stream.haml b/app/views/attachments/destroy.turbo_stream.haml
index 48cee0bc3..e66582528 100644
--- a/app/views/attachments/destroy.turbo_stream.haml
+++ b/app/views/attachments/destroy.turbo_stream.haml
@@ -1,7 +1,9 @@
= 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}"
+
+- 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"
diff --git a/app/views/commencer/show.html.haml b/app/views/commencer/show.html.haml
index 55b7a35c9..545789f98 100644
--- a/app/views/commencer/show.html.haml
+++ b/app/views/commencer/show.html.haml
@@ -13,7 +13,11 @@
#{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'
+ = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
+
- else
+ = render ProcedureDraftWarningComponent.new(revision: @revision, current_administrateur:, extra_class_names: "fr-mb-2w")
+
- if @prefilled_dossier
= render Dsfr::CalloutComponent.new(title: t(".prefilled_draft"), heading_level: 'h2') do |c|
- c.with_body do
diff --git a/app/views/experts/avis/show.html.haml b/app/views/experts/avis/show.html.haml
index 3704bedac..7ae1c1e90 100644
--- a/app/views/experts/avis/show.html.haml
+++ b/app/views/experts/avis/show.html.haml
@@ -2,4 +2,9 @@
= 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' }
diff --git a/app/views/users/dossiers/demande.html.haml b/app/views/users/dossiers/demande.html.haml
index 543b3c867..ef21d650e 100644
--- a/app/views/users/dossiers/demande.html.haml
+++ b/app/views/users/dossiers/demande.html.haml
@@ -6,20 +6,15 @@
.dossier-container.fr-mb-4w
= 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-grid-row.fr-grid-row--center
.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' }
-
- - if !@dossier.read_only?
- .fr-container.fr-mt-2w
- .fr-grid-row.fr-grid-row--center
- .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')
+ - if !@dossier.read_only?
+ .fr-px-2w.fr-mt-2w
+ %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')
diff --git a/bun.lockb b/bun.lockb
index beab4b760..59fe8acc1 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index f110f4cb3..a38b32cae 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,3 @@
# 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
diff --git a/config/locales/views/layouts/_breadcrumb.fr.yml b/config/locales/views/layouts/_breadcrumb.fr.yml
index 9605fb9ef..2cb0ed409 100644
--- a/config/locales/views/layouts/_breadcrumb.fr.yml
+++ b/config/locales/views/layouts/_breadcrumb.fr.yml
@@ -10,7 +10,7 @@ fr:
since: "depuis le %{date}"
closed: "Close"
published: "Publiée"
- draft: "En test"
+ draft: "En test"
more_info_on_test: "Pour plus d’information sur la phase de test"
go_to_FAQ: "consulter la FAQ"
url_FAQ: "/faq#accordion-administrateur-2"
diff --git a/package.json b/package.json
index d301bee84..fc11c6e57 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,9 @@
"@coldwired/react": "^0.15.0",
"@coldwired/turbo-stream": "^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",
- "@graphiql/plugin-explorer": "^3.0.2",
+ "@graphiql/plugin-explorer": "^3.1.0",
"@graphiql/toolkit": "^0.9.1",
"@headlessui/react": "^1.6.6",
"@heroicons/react": "^1.0.6",
@@ -15,11 +15,11 @@
"@hotwired/turbo": "^7.3.0",
"@mapbox/mapbox-gl-draw": "^1.3.0",
"@popperjs/core": "^2.11.8",
- "@rails/actiontext": "^7.1.3-2",
- "@rails/activestorage": "^7.1.3-2",
- "@rails/ujs": "^7.1.3-2",
+ "@rails/actiontext": "^7.1.3-4",
+ "@rails/activestorage": "^7.1.3-4",
+ "@rails/ujs": "^7.1.3-4",
"@reach/slider": "^0.17.0",
- "@sentry/browser": "8.7.0",
+ "@sentry/browser": "8.15.0",
"@tiptap/core": "^2.2.4",
"@tiptap/extension-bold": "^2.2.4",
"@tiptap/extension-bullet-list": "^2.2.4",
@@ -44,26 +44,26 @@
"@tmcw/togeojson": "^5.6.0",
"chartkick": "^5.0.1",
"core-js": "^3.37.1",
- "date-fns": "^2.30.0",
- "debounce": "^1.2.1",
+ "date-fns": "^3.6.0",
+ "debounce": "^2.1.0",
"geojson": "^0.5.0",
- "graphiql": "^3.2.3",
- "graphql": "^16.8.1",
+ "graphiql": "^3.3.2",
+ "graphql": "^16.9.0",
"highcharts": "^10.3.3",
"lightgallery": "^2.7.2",
"maplibre-gl": "^1.15.2",
"match-sorter": "^6.3.4",
"patch-package": "^8.0.0",
- "react": "^18.3.0",
- "react-aria-components": "^1.2.0",
+ "react": "^18.3.1",
+ "react-aria-components": "^1.2.1",
"react-coordinate-input": "^1.0.0",
- "react-dom": "^18.3.0",
+ "react-dom": "^18.3.1",
"react-popper": "^2.3.0",
"react-use-event-hook": "^0.9.6",
"spectaql": "^2.3.1",
"stimulus-use": "^0.52.2",
- "superstruct": "^1.0.4",
- "terser": "^5.31.0",
+ "superstruct": "^2.0.2",
+ "terser": "^5.31.1",
"tiny-invariant": "^1.3.3",
"tippy.js": "^6.3.7",
"trix": "^1.2.3",
@@ -73,7 +73,7 @@
"@esbuild/darwin-arm64": "=0.19.9",
"@esbuild/linux-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-linux-x64-gnu": "=4.9.1",
"@rollup/rollup-win32-x64-msvc": "=4.9.1",
@@ -83,28 +83,27 @@
"@types/mapbox__mapbox-gl-draw": "^1.2.5",
"@types/rails__activestorage": "^7.1.1",
"@types/rails__ujs": "^6.0.4",
- "@types/react": "^18.2.79",
- "@types/react-dom": "^18.2.25",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
"@types/sortablejs": "^1.15.8",
- "@typescript-eslint/eslint-plugin": "^7.11.0",
- "@typescript-eslint/parser": "^7.11.0",
- "@vitejs/plugin-react": "^4.3.0",
+ "@typescript-eslint/eslint-plugin": "^7.15.0",
+ "@typescript-eslint/parser": "^7.15.0",
+ "@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
- "axe-core": "^4.8.4",
"del-cli": "^5.1.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"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",
- "jsdom": "^22.1.0",
- "postcss": "^8.4.38",
- "prettier": "^3.3.0",
- "typescript": "^5.4.5",
- "vite": "^5.2.12",
+ "jsdom": "^24.1.0",
+ "postcss": "^8.4.39",
+ "prettier": "^3.3.2",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.3",
"vite-plugin-full-reload": "^1.1.0",
"vite-plugin-ruby": "^5.0.0",
- "vitest": "^1.6.0"
+ "vitest": "^2.0.0"
},
"scripts": {
"clean": "del tmp public/graphql && bin/vite clobber",
diff --git a/spec/components/attachment/multiple_component_spec.rb b/spec/components/attachment/multiple_component_spec.rb
index 26dfacd2c..143831280 100644
--- a/spec/components/attachment/multiple_component_spec.rb
+++ b/spec/components/attachment/multiple_component_spec.rb
@@ -75,8 +75,8 @@ RSpec.describe Attachment::MultipleComponent, type: :component do
context 'max attachments' do
let(:kwargs) { { max: 1 } }
- it 'does not render visible input file where max attachments has been reached' do
- expect(subject).to have_selector('.hidden input[type=file]')
+ it 'renders a disabled input file where max attachments has been reached' do
+ expect(subject).to have_selector('input[type=file][disabled]')
end
end
diff --git a/spec/models/logic/champ_value_spec.rb b/spec/models/logic/champ_value_spec.rb
index f7b12020f..c287c57d4 100644
--- a/spec/models/logic/champ_value_spec.rb
+++ b/spec/models/logic/champ_value_spec.rb
@@ -40,12 +40,6 @@ describe Logic::ChampValue do
it { is_expected.to be nil }
end
-
- context 'with invalid value' do
- before { champ.value = 'environ 300' }
-
- it { is_expected.to be nil }
- end
end
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 { 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
context 'dropdown tdc' do
diff --git a/spec/views/administrateurs/procedures/show.html.haml_spec.rb b/spec/views/administrateurs/procedures/show.html.haml_spec.rb
index 57224b13b..da9cbe1e6 100644
--- a/spec/views/administrateurs/procedures/show.html.haml_spec.rb
+++ b/spec/views/administrateurs/procedures/show.html.haml_spec.rb
@@ -16,17 +16,11 @@ describe 'administrateurs/procedures/show', type: :view do
render
end
- describe 'publish button is visible' do
- it { expect(rendered).to have_css('#publish-procedure-link') }
- it { expect(rendered).not_to have_css('#close-procedure-link') }
- end
-
- 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') }
+ it "render content" do
+ expect(rendered).to have_css('#publish-procedure-link')
+ expect(rendered).not_to have_css('#close-procedure-link')
+ expect(rendered).to have_content('En test')
+ expect(rendered).not_to have_css('#archive-procedure')
end
end
end
diff --git a/spec/views/commencer/show.html.haml_spec.rb b/spec/views/commencer/show.html.haml_spec.rb
index 571654477..bcda8b893 100644
--- a/spec/views/commencer/show.html.haml_spec.rb
+++ b/spec/views/commencer/show.html.haml_spec.rb
@@ -7,10 +7,15 @@ RSpec.describe 'commencer/show', type: :view do
let(:drafts) { [] }
let(:not_drafts) { [] }
let(:preview_dossiers) { dossiers.take(3) }
+ let(:user) { nil }
+
+ before do
+ allow(view).to receive(:current_administrateur).and_return(user&.administrateur)
+ end
before do
assign(:procedure, procedure)
- assign(:revision, procedure.published_revision)
+ assign(:revision, procedure.active_revision)
assign(:dossiers, dossiers)
assign(:drafts, drafts)
assign(:not_drafts, not_drafts)
@@ -25,8 +30,6 @@ RSpec.describe 'commencer/show', type: :view do
subject { render }
context 'when no user is signed in' do
- let(:user) { nil }
-
it 'renders sign-in and sign-up links' do
subject
expect(rendered).to have_link('Créer un compte')
@@ -98,4 +101,35 @@ RSpec.describe 'commencer/show', type: :view do
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
diff --git a/vite.config.ts b/vite.config.ts
index f0f0dfde8..48ab90a9c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,7 +2,7 @@ import { defineConfig } from 'vite';
import ViteReact from '@vitejs/plugin-react';
import RubyPlugin from 'vite-plugin-ruby';
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 = [
RubyPlugin(),
@@ -10,13 +10,13 @@ const plugins = [
FullReload(
['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'],
{ delay: 200 }
- ),
- {
- ...optimizeLocales.vite({
- locales: ['en-GB', 'fr-FR']
- }),
- enforce: 'pre' as const
- }
+ )
+ // {
+ // ...optimizeLocales.vite({
+ // locales: ['en-GB', 'fr-FR']
+ // }),
+ // enforce: 'pre' as const
+ // }
];
export default defineConfig({