diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d64df3a4b..fae53c08b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -307,9 +307,9 @@ class ApplicationController < ActionController::Base DS_SIGN_IN_COUNT: current_user&.sign_in_count, DS_CREATED_AT: current_administrateur&.created_at, DS_ID: current_administrateur&.id, - DS_NB_DEMARCHES_BROUILLONS: nb_demarches_by_state['brouillon'], - DS_NB_DEMARCHES_ACTIVES: nb_demarches_by_state['publiee'], - DS_NB_DEMARCHES_ARCHIVES: nb_demarches_by_state['close'] + DS_NB_DEMARCHES_BROUILLONS: nb_demarches_by_state['brouillon'] || 0, + DS_NB_DEMARCHES_ACTIVES: nb_demarches_by_state['publiee'] || 0, + DS_NB_DEMARCHES_ARCHIVES: nb_demarches_by_state['close'] || 0 } } end diff --git a/app/javascript/components/shared/queryClient.ts b/app/javascript/components/shared/queryClient.ts index ebf797c62..2ab492c4a 100644 --- a/app/javascript/components/shared/queryClient.ts +++ b/app/javascript/components/shared/queryClient.ts @@ -1,18 +1,7 @@ import { QueryClient, QueryFunction } from 'react-query'; -import { httpRequest, isNumeric } from '@utils'; +import { httpRequest, isNumeric, getConfig } from '@utils'; import { matchSorter } from 'match-sorter'; -type Gon = { - gon: { - autocomplete?: { - api_geo_url?: string; - api_adresse_url?: string; - api_education_url?: string; - }; - }; -}; -declare const window: Window & typeof globalThis & Gon; - const API_EDUCATION_QUERY_LIMIT = 5; const API_GEO_QUERY_LIMIT = 5; const API_ADRESSE_QUERY_LIMIT = 5; @@ -26,8 +15,9 @@ const API_ADRESSE_QUERY_LIMIT = 5; // NB: 60 is arbitrary, we may add more if needed. const API_GEO_COMMUNES_QUERY_LIMIT = 60; -const { api_geo_url, api_adresse_url, api_education_url } = - window.gon.autocomplete || {}; +const { + autocomplete: { api_geo_url, api_adresse_url, api_education_url } +} = getConfig(); type QueryKey = readonly [ scope: string, diff --git a/app/javascript/controllers/autosave_controller.ts b/app/javascript/controllers/autosave_controller.ts index 9e3c3abeb..d63dcde37 100644 --- a/app/javascript/controllers/autosave_controller.ts +++ b/app/javascript/controllers/autosave_controller.ts @@ -1,5 +1,4 @@ -import { httpRequest, ResponseError } from '@utils'; -import { z } from 'zod'; +import { httpRequest, ResponseError, getConfig } from '@utils'; import { ApplicationController } from './application_controller'; import { AutoUpload } from '../shared/activestorage/auto-upload'; @@ -9,10 +8,9 @@ import { ERROR_CODE_READ } from '../shared/activestorage/file-upload-error'; -const Gon = z.object({ autosave: z.object({ debounce_delay: z.number() }) }); - -declare const window: Window & typeof globalThis & { gon: unknown }; -const { debounce_delay } = Gon.parse(window.gon).autosave; +const { + autosave: { debounce_delay } +} = getConfig(); const AUTOSAVE_DEBOUNCE_DELAY = debounce_delay; const AUTOSAVE_TIMEOUT_DELAY = 60000; diff --git a/app/javascript/controllers/autosave_status_controller.ts b/app/javascript/controllers/autosave_status_controller.ts index 4173740f7..e204e5c67 100644 --- a/app/javascript/controllers/autosave_status_controller.ts +++ b/app/javascript/controllers/autosave_status_controller.ts @@ -4,19 +4,15 @@ import { hasClass, addClass, removeClass, - ResponseError + ResponseError, + getConfig } from '@utils'; -import { z } from 'zod'; import { ApplicationController } from './application_controller'; -const Gon = z.object({ - autosave: z.object({ status_visible_duration: z.number() }) -}); - -declare const window: Window & typeof globalThis & { gon: unknown }; -const { status_visible_duration } = Gon.parse(window.gon).autosave; - +const { + autosave: { status_visible_duration } +} = getConfig(); const AUTOSAVE_STATUS_VISIBLE_DURATION = status_visible_duration; // This is a controller we attach to the status area in the main form. It diff --git a/app/javascript/shared/track/crisp.js b/app/javascript/shared/track/crisp.ts similarity index 77% rename from app/javascript/shared/track/crisp.js rename to app/javascript/shared/track/crisp.ts index f03546df6..49cdeca6c 100644 --- a/app/javascript/shared/track/crisp.js +++ b/app/javascript/shared/track/crisp.ts @@ -1,4 +1,16 @@ -const { key, enabled, administrateur } = gon.crisp || {}; +import { getConfig } from '@utils'; +const { + crisp: { key, enabled, administrateur } +} = getConfig(); + +declare const window: Window & + typeof globalThis & { + CRISP_WEBSITE_ID?: string | null; + $crisp: ( + | [cmd: string, key: string, value: unknown] + | [key: string, value: unknown] + )[]; + }; if (enabled) { window.$crisp = []; @@ -10,7 +22,7 @@ if (enabled) { script.id = 'crisp-js'; script.async = true; script.src = 'https://client.crisp.chat/l.js'; - firstScript.parentNode.insertBefore(script, firstScript); + firstScript.parentNode?.insertBefore(script, firstScript); window.$crisp.push(['set', 'user:email', [administrateur.email]]); window.$crisp.push(['set', 'session:segments', [['administrateur']]]); diff --git a/app/javascript/shared/track/matomo.js b/app/javascript/shared/track/matomo.ts similarity index 80% rename from app/javascript/shared/track/matomo.js rename to app/javascript/shared/track/matomo.ts index c3ca679e6..74627e709 100644 --- a/app/javascript/shared/track/matomo.js +++ b/app/javascript/shared/track/matomo.ts @@ -1,4 +1,11 @@ -const { cookieDomain, domain, enabled, host, key } = gon.matomo || {}; +import { getConfig } from '@utils'; + +const { + matomo: { cookieDomain, domain, enabled, host, key } +} = getConfig(); + +declare const window: Window & + typeof globalThis & { _paq: [key: string, value: unknown] }; if (enabled) { window._paq = window._paq || []; @@ -30,5 +37,5 @@ if (enabled) { script.id = 'matomo-js'; script.async = true; script.src = jsUrl; - firstScript.parentNode.insertBefore(script, firstScript); + firstScript.parentNode?.insertBefore(script, firstScript); } diff --git a/app/javascript/shared/track/sentry.js b/app/javascript/shared/track/sentry.ts similarity index 83% rename from app/javascript/shared/track/sentry.js rename to app/javascript/shared/track/sentry.ts index 864fa1d73..154fe3e84 100644 --- a/app/javascript/shared/track/sentry.js +++ b/app/javascript/shared/track/sentry.ts @@ -1,6 +1,9 @@ import * as Sentry from '@sentry/browser'; +import { getConfig } from '@utils'; -const { key, enabled, user, environment, browser } = gon.sentry || {}; +const { + sentry: { key, enabled, user, environment, browser } +} = getConfig(); // We need to check for key presence here as we do not have a dsn for browser yet if (enabled && key) { @@ -22,7 +25,7 @@ if (enabled && key) { // Register a way to explicitely capture messages from a different bundle. addEventListener('sentry:capture-exception', (event) => { - const error = event.detail; + const error = (event as CustomEvent).detail; Sentry.captureException(error); }); } diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index f4f463ef6..1ac5917a0 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -1,10 +1,75 @@ import Rails from '@rails/ujs'; import debounce from 'debounce'; import { session } from '@hotwired/turbo'; +import { z } from 'zod'; export { debounce }; export const { fire, csrfToken, cspNonce } = Rails; +const Gon = z + .object({ + autosave: z + .object({ + debounce_delay: z.number().default(0), + status_visible_duration: z.number().default(0) + }) + .default({}), + autocomplete: z + .object({ + api_geo_url: z.string().optional(), + api_adresse_url: z.string().optional(), + api_education_url: z.string().optional() + }) + .default({}), + matomo: z + .object({ + cookieDomain: z.string().optional(), + domain: z.string().optional(), + enabled: z.boolean().default(false), + host: z.string().optional(), + key: z.string().nullish() + }) + .default({}), + sentry: z + .object({ + key: z.string().nullish(), + enabled: z.boolean().default(false), + environment: z.string().optional(), + user: z.object({ id: z.string() }).default({ id: '' }), + browser: z.object({ modern: z.boolean() }).default({ modern: false }) + }) + .default({}), + crisp: z + .object({ + key: z.string().nullish(), + enabled: z.boolean().default(false), + administrateur: z + .object({ + email: z.string(), + DS_SIGN_IN_COUNT: z.number(), + DS_NB_DEMARCHES_BROUILLONS: z.number(), + DS_NB_DEMARCHES_ACTIVES: z.number(), + DS_NB_DEMARCHES_ARCHIVES: z.number(), + DS_ID: z.number() + }) + .default({ + email: '', + DS_SIGN_IN_COUNT: 0, + DS_NB_DEMARCHES_BROUILLONS: 0, + DS_NB_DEMARCHES_ACTIVES: 0, + DS_NB_DEMARCHES_ARCHIVES: 0, + DS_ID: 0 + }) + }) + .default({}) + }) + .default({}); +declare const window: Window & typeof globalThis & { gon: unknown }; + +export function getConfig() { + return Gon.parse(window.gon); +} + export function show(el: HTMLElement | null) { el?.classList.remove('hidden'); } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 8e1a4fa1f..9aa34f211 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -13,6 +13,8 @@ = favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32") = favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96") + = Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce) + = vite_client_tag = vite_react_refresh_tag = vite_javascript_tag 'application' @@ -31,8 +33,6 @@ = stylesheet_link_tag 'application', media: 'all' - = Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce) - %body{ id: content_for(:page_id), class: browser.platform.ios? ? 'ios' : nil } .page-wrapper = render partial: "layouts/outdated_browser_banner" diff --git a/app/views/layouts/component_preview.html.haml b/app/views/layouts/component_preview.html.haml index ada6600eb..68c49f5d6 100644 --- a/app/views/layouts/component_preview.html.haml +++ b/app/views/layouts/component_preview.html.haml @@ -13,6 +13,8 @@ = favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32") = favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96") + = vite_client_tag + = vite_react_refresh_tag = vite_javascript_tag 'application' = preload_link_tag(asset_url("Muli-Regular.woff2"))