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({