Bladeren bron

Merge branch 'develop' into feature/freemium

# Conflicts:
#	i18n/lang/fr/general.json
#	pages/parameters/attendances.vue
#	pages/subscription.vue
Vincent 6 maanden geleden
bovenliggende
commit
8f916391ec
43 gewijzigde bestanden met toevoegingen van 698 en 349 verwijderingen
  1. 6 3
      .gitlab-ci.yml
  2. 9 0
      assets/css/global.scss
  3. 48 0
      components/Layout/Dialog/RefreshNeeded.vue
  4. 22 2
      components/Layout/Header.vue
  5. 4 0
      components/Layout/Header/Menu.vue
  6. 39 0
      components/Layout/MobytStatus.vue
  7. 11 3
      components/Ui/Form.vue
  8. 0 11
      components/Ui/Input/Autocomplete/Accesses.vue
  9. 19 0
      config/abilities/pages/myAccount.yaml
  10. 9 2
      i18n/lang/fr/general.json
  11. 2 0
      layouts/default.vue
  12. 2 0
      layouts/parameters.vue
  13. 1 1
      middleware/routing.global.ts
  14. 3 0
      models/Organization/Parameters.ts
  15. 29 0
      pages/dev/poc_refresh_needed.vue
  16. 10 4
      pages/parameters/attendances.vue
  17. 4 46
      pages/subscription.vue
  18. 15 3
      plugins/init.server.ts
  19. 6 4
      services/data/normalizer/hydraNormalizer.ts
  20. 10 2
      services/layout/menuBuilder/abstractMenuBuilder.ts
  21. 12 2
      services/layout/menuBuilder/accountMenuBuilder.ts
  22. 81 67
      services/layout/menuBuilder/configurationMenuBuilder.ts
  23. 5 0
      stores/accessProfile.ts
  24. 7 1
      stores/sse.ts
  25. 19 2
      tests/units/services/layout/menuBuilder/abstractMenuBuilder.test.ts
  26. 7 0
      tests/units/services/layout/menuBuilder/accessMenuBuilder.test.ts
  27. 117 92
      tests/units/services/layout/menuBuilder/accountMenuBuilder.test.ts
  28. 7 0
      tests/units/services/layout/menuBuilder/admin2iosMenuBuilder.test.ts
  29. 2 0
      tests/units/services/layout/menuBuilder/agendaMenuBuilder.test.ts
  30. 3 0
      tests/units/services/layout/menuBuilder/basicomptaMenuBuilder.test.ts
  31. 10 0
      tests/units/services/layout/menuBuilder/billingMenuBuilder.test.ts
  32. 3 0
      tests/units/services/layout/menuBuilder/communicationMenuBuilder.test.ts
  33. 130 104
      tests/units/services/layout/menuBuilder/configurationMenuBuilder.test.ts
  34. 17 0
      tests/units/services/layout/menuBuilder/cotisationsMenuBuilder.test.ts
  35. 1 0
      tests/units/services/layout/menuBuilder/donorsMenuBuilder.test.ts
  36. 6 0
      tests/units/services/layout/menuBuilder/educationalMenuBuilder.test.ts
  37. 1 0
      tests/units/services/layout/menuBuilder/equipmentMenuBuilder.test.ts
  38. 3 0
      tests/units/services/layout/menuBuilder/myAccessesMenuBuilder.test.ts
  39. 4 0
      tests/units/services/layout/menuBuilder/myFamilyMenuBuilder.test.ts
  40. 4 0
      tests/units/services/layout/menuBuilder/statsMenuBuilder.test.ts
  41. 1 0
      tests/units/services/layout/menuBuilder/websiteAdminMenuBuilder.test.ts
  42. 7 0
      tests/units/services/layout/menuBuilder/websiteListMenuBuilder.test.ts
  43. 2 0
      types/layout.d.ts

+ 6 - 3
.gitlab-ci.yml

@@ -5,12 +5,14 @@ stages:
 
 variables:
   APP_ENV: ci
+  GIT_CLEAN_FLAGS: -ffdx
+  YARN_ENABLE_GLOBAL_CACHE: 'false'
+  YARN_ENABLE_TELEMETRY: 'false'
 
 cache:
+  key: ${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_ID}
   paths:
     - ./node_modules
-    - .yarn
-    - yarn.lock
 
 build_image:
   stage: build
@@ -26,9 +28,10 @@ build_image:
 .default_config: &default_config
   image: $CI_REGISTRY_IMAGE:latest
   before_script:
+    #- rm -rf node_modules/ .yarn/ || true
     - echo "" > ./env/local.app.opentalent.fr.crt
     - echo "" > ./env/local.app.opentalent.fr.key
-    - yarn install --network-timeout 10000
+    - yarn install --frozen-lockfile --network-timeout 60000
     - HOSTNAME=ci yarn prepare
 
 unit:

+ 9 - 0
assets/css/global.scss

@@ -63,6 +63,10 @@ header .v-toolbar__content {
       border-bottom: 1px solid;
       border-bottom-color: rgb(var(--v-theme-neutral));
     }
+
+    .v-list-item.end-of-section {
+      border-bottom: 3px solid rgb(var(--v-theme-neutral));
+    }
   }
 }
 
@@ -86,6 +90,11 @@ h4 {
   color: rgb(var(--v-theme-on-neutral));
 }
 
+h5 {
+  font-size: 1rem;
+  color: rgb(var(--v-theme-on-neutral));
+}
+
 // Encart informatif
 .explanation {
   display: flex;

+ 48 - 0
components/Layout/Dialog/RefreshNeeded.vue

@@ -0,0 +1,48 @@
+<!--
+Une boite de dialogue signalant que la page doit être rechargée (par exemple
+parce que le accessProfile a été modifié dans un autre onglet).
+-->
+<template>
+  <LazyLayoutDialog :show="showRefreshNeededDialog" theme="info">
+    <template #dialogType>{{ $t('information') }}</template>
+    <template #dialogTitle>{{ $t('refresh_needed') }}</template>
+    <template #dialogText>
+      <v-card-text class="text">
+        <p>
+          {{ $t('refresh_needed_message') }}
+        </p>
+      </v-card-text>
+    </template>
+    <template #dialogBtn>
+      <v-btn class="submitBtn theme-info" @click="refreshPage">
+        {{ $t('refresh_page') }}
+      </v-btn>
+    </template>
+  </LazyLayoutDialog>
+</template>
+
+<script setup lang="ts">
+import { useAccessProfileStore } from '~/stores/accessProfile'
+
+const accessProfileUpdated = ref(false)
+
+const accessProfileStore = useAccessProfileStore()
+const pageStore = usePageStore()
+
+const showRefreshNeededDialog = computed(
+  () => accessProfileUpdated.value && !pageStore.loading,
+)
+
+onMounted(() => {
+  accessProfileStore.$subscribe(() => {
+    accessProfileUpdated.value = true
+  })
+})
+
+const refreshPage = () => {
+  pageStore.loading = true
+  window.location.reload()
+}
+</script>
+
+<style scoped lang="scss"></style>

+ 22 - 2
components/Layout/Header.vue

@@ -29,7 +29,22 @@ Contient entre autres le nom de l'organisation, l'accès à l'aide et aux préf
 
     <LayoutHeaderHomeBtn v-if="smAndUp" />
 
-    <LayoutHeaderMenu name="WebsiteList" :translate-label="false" />
+    <LayoutHeaderMenu
+      v-if="isWebsitesMenuNotEmpty"
+      name="WebsiteList"
+      :translate-label="false"
+    />
+
+    <v-btn
+      v-else
+      icon
+      size="small"
+      class="ml-2"
+      href="https://opentalent.fr"
+      target="_blank"
+    >
+      <v-icon icon="fas fa-globe-americas" class="on-primary" />
+    </v-btn>
 
     <LayoutHeaderMenu name="MyAccesses" />
 
@@ -73,7 +88,7 @@ const title: ComputedRef<string> = computed(
   () => organizationProfile.name ?? 'Opentalent',
 )
 
-const { hasMenu, isMenuOpened, toggleMenu } = useMenu()
+const { hasMenu, isMenuOpened, toggleMenu, getMenu } = useMenu()
 
 const { smAndUp } = useDisplay()
 
@@ -113,6 +128,11 @@ const showUniversalButton =
   ability.can('manage', 'equipments')
 
 const layoutStore = useLayoutStore()
+
+const websitesMenu = getMenu('WebsiteList')
+const isWebsitesMenuNotEmpty = computed(
+  () => websitesMenu?.children?.length > 0,
+)
 </script>
 
 <style scoped>

+ 4 - 0
components/Layout/Header/Menu.vue

@@ -41,6 +41,10 @@ header principal (configuration, paramètres du compte...)
                 :id="child.label"
                 :href="!isInternalLink(child) ? child.to : undefined"
                 :to="isInternalLink(child) ? child.to : undefined"
+                :class="{
+                  'end-of-section':
+                    'endOfSubsection' in child && child.endOfSubsection,
+                }"
               >
                 <span v-if="child.icon" class="pr-2 d-flex align-center">
                   <v-avatar

+ 39 - 0
components/Layout/MobytStatus.vue

@@ -0,0 +1,39 @@
+<template>
+  <v-col cols="12" lg="12">
+    <strong>{{ $t('remaining_sms_credit') }}</strong> -
+    <span v-if="!mobytPending && mobytStatus !== null && mobytStatus.active">
+      {{
+        mobytStatus.money.toLocaleString($i18n.locale, {
+          style: 'currency',
+          currency: 'EUR',
+        })
+      }}
+      {{
+        i18n.t('convert_price_to_sms', {
+          nb_sms: mobytStatus.amount,
+        })
+      }}
+    </span>
+  </v-col>
+</template>
+
+<script setup lang="ts">
+import type { Ref } from 'vue'
+import type { AsyncData } from '#app'
+import { useAbility } from '@casl/vue'
+import { useEntityFetch } from '~/composables/data/useEntityFetch'
+import { useOrganizationProfileStore } from '~/stores/organizationProfile'
+import MobytUserStatus from '~/models/Organization/MobytUserStatus'
+
+const ability = useAbility()
+const { fetch } = useEntityFetch()
+const i18n = useI18n()
+const organizationProfile = useOrganizationProfileStore()
+
+const { data: mobytStatus, pending: mobytPending } = fetch(
+  MobytUserStatus,
+  organizationProfile.id,
+) as AsyncData<MobytUserStatus | null, Error | null>
+</script>
+
+<style scoped lang="scss"></style>

+ 11 - 3
components/Ui/Form.vue

@@ -396,9 +396,17 @@ const validate = async function () {
 }
 
 // #### Gestion de l'état dirty
-watch(props.modelValue, async (newEntity, oldEntity) => {
-  setIsDirty(true)
-})
+watch(
+  // /!\ Important de passer par un getter de l'objet (le `() => ({ ...props.modelValue })`),
+  //     car on perd la réactivité de celui-ci quand on soumet le formulaire
+  //     (et donc le watcher cesse de fonctionner)
+  () => ({ ...props.modelValue }),
+  (newEntity, oldEntity) => {
+    if (JSON.stringify(newEntity) !== JSON.stringify(oldEntity)) {
+      setIsDirty(true)
+    }
+  },
+)
 
 /**
  * Handle events if the form is dirty to prevent submission

+ 0 - 11
components/Ui/Input/Autocomplete/Accesses.vue

@@ -21,7 +21,6 @@ Champs autocomplete dédié à la recherche des Accesses d'une structure
       prepend-inner-icon="fas fa-magnifying-glass"
       :return-object="false"
       :variant="variant"
-      :class="pending || pageStore.loading ? 'hide-selection' : ''"
       @update:model-value="onUpdateModelValue"
       @update:search="onUpdateSearch"
     />
@@ -268,14 +267,4 @@ onBeforeUnmount(() => {
 .v-autocomplete {
   min-width: 350px;
 }
-
-.hide-selection {
-  /**
-      On cache le contenu au chargement en attendant de résoudre le bug qui fait
-      que ce sont les ids ou les IRIs qui s'affichent le temps du chargement
-   */
-  :deep(.v-chip__content) {
-    color: transparent !important;
-  }
-}
 </style>

+ 19 - 0
config/abilities/pages/myAccount.yaml

@@ -89,6 +89,25 @@ subscription_page:
           ],
       }
 
+subscription_page_sms_section:
+  action: 'display'
+  conditions:
+    - {
+        function: organizationHasAllModules,
+        parameters: ['GeneralConfig', 'Sms'],
+      }
+    - {
+        function: accessHasAnyProfile,
+        parameters:
+          [
+            'admin',
+            'administratifManager',
+            'pedagogicManager',
+            'financialManager',
+            'caMember',
+          ],
+      }
+
 my_bills_page:
   action: 'display'
   conditions:

+ 9 - 2
i18n/lang/fr/general.json

@@ -32,6 +32,9 @@
   "freemium_profile_page": "Mon profile",
   "freemium_dashboard_page": "Bienvenue sur votre compte Opentalent",
   "freemium_page": "Freemium",
+  "showing": "Affichage",
+  "alert": "Alertes",
+  "handlePresence": "Affichage de la colonne \"présence\" dans le tableau de gestion des présences/absences/retards",
   "i_subscribe": "Je m'abonne",
   "price_include_cmf": "Inclus avec votre adhésion CMF",
   "artist": "Artist Standard",
@@ -44,7 +47,7 @@
   "opentalent_options": "Les options Opentalent",
   "opentalent_offers": "Les offres Opentalent",
   "service_detail": "Détail des services",
-  "my_settings_page": "Mes paramètres",
+  "my_settings_page": "Mes préférences",
   "allow_report_message": "Je souhaite recevoir les rapports d'envoi des emails que j'envoie",
   "my-settings_breadcrumbs": "Mes paramètres",
   "message_settings": "Paramètres des messages",
@@ -795,5 +798,9 @@
   "try_premium_version": "Essayer la version premium",
   "subscribe_to_the_offer": "Souscrire à l'offre",
   "to_know_more": "En savoir plus",
-  "placeListMenuKey": "Lieu"
+  "placeListMenuKey": "Lieu",
+  "information": "Information",
+  "refresh_needed": "Actualisation requise",
+  "refresh_needed_message": "La page a besoin d'être actualisée pour afficher les dernières modifications.",
+  "refresh_page": "Actualiser la page"
 }

+ 2 - 0
layouts/default.vue

@@ -6,6 +6,8 @@
     <v-app>
       <LayoutLoadingScreen />
 
+      <LayoutDialogRefreshNeeded />
+
       <LayoutHeader />
 
       <LayoutMainMenu>

+ 2 - 0
layouts/parameters.vue

@@ -6,6 +6,8 @@
     <v-app>
       <LayoutLoadingScreen />
 
+      <LayoutDialogRefreshNeeded />
+
       <LayoutHeader />
 
       <LayoutParametersMenu />

+ 1 - 1
middleware/routing.global.ts

@@ -19,7 +19,7 @@ export default defineNuxtRouteMiddleware((to, _) => {
     const runtimeConfig = useRuntimeConfig()
     if (
       runtimeConfig.public.env === 'production' &&
-      (name === 'cmf_licence_page')
+      name === 'cmf_licence_page'
     ) {
       const { redirectToHome } = useRedirect()
       redirectToHome()

+ 3 - 0
models/Organization/Parameters.ts

@@ -167,4 +167,7 @@ export default class Parameters extends ApiModel {
 
   @Attr([])
   declare subdomains: []
+
+  @Bool(false, { notNullable: false })
+  declare handlePresence: boolean
 }

+ 29 - 0
pages/dev/poc_refresh_needed.vue

@@ -0,0 +1,29 @@
+<!--
+La boite de dialogue 'RefreshNeeded' est supposé s'afficher dès lors
+que l'accessProfile est modifié.
+-->
+
+<template>
+  <div>
+    <h1>POC Refresh Needed</h1>
+
+    <LayoutDialogRefreshNeeded />
+
+    <v-btn @click="onUpdateClick">Update profile</v-btn>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useAccessProfileStore } from '~/stores/accessProfile'
+
+definePageMeta({
+  layout: false,
+})
+
+const accessProfile = useAccessProfileStore()
+
+const onUpdateClick = () => {
+  accessProfile.setActivityYear(accessProfile.activityYear + 1)
+  console.log('activity year updated to ' + accessProfile.activityYear)
+}
+</script>

+ 10 - 4
pages/parameters/attendances.vue

@@ -7,13 +7,19 @@
           <div v-if="parameters">
             <v-row>
               <v-col cols="12">
+                <h5 class="pa-2">{{ $t('showing') }}</h5>
                 <UiInputCheckbox
-                  v-model="parameters.sendAttendanceEmail"
-                  field="sendAttendanceEmail"
-                  label="sendAttendanceEmail"
+                  v-model="parameters.handlePresence"
+                  field="handlePresence"
                 />
+            <h5 class="pa-2">{{ $t('alert') }}</h5>
+              <UiInputCheckbox
+                v-model="parameters.sendAttendanceEmail"
+                field="sendAttendanceEmail"
+                label="sendAttendanceEmail"
+              />
 
-                <UiInputCheckbox
+              <UiInputCheckbox
                   v-model="parameters.sendAttendanceSms"
                   field="sendAttendanceSms"
                 />

+ 4 - 46
pages/subscription.vue

@@ -41,26 +41,9 @@ Page 'Mon abonnement'
                 {{ $d(line.dateEnd) }}
               </v-col>
 
-              <v-col v-if="ability.can('manage', 'texto')" cols="12" lg="12">
-                <strong>{{ $t('remaining_sms_credit') }}</strong> -
-                <span
-                  v-if="
-                    mobytPendingStatus == FETCHING_STATUS.SUCCESS && mobytStatus !== null && mobytStatus.active
-                  "
-                >
-                  {{
-                    mobytStatus.money.toLocaleString($i18n.locale, {
-                      style: 'currency',
-                      currency: 'EUR',
-                    })
-                  }}
-                  {{
-                    i18n.t('convert_price_to_sms', {
-                      nb_sms: mobytStatus.amount,
-                    })
-                  }}
-                </span>
-              </v-col>
+              <LayoutMobytStatus
+                v-if="ability.can('display', 'subscription_page_sms_section')"
+              />
             </v-row>
           </v-container>
         </UiExpansionPanel>
@@ -488,14 +471,12 @@ import type { AsyncData } from '#app'
 import { useOrganizationProfileStore } from '~/stores/organizationProfile'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import DolibarrAccount from '~/models/Organization/DolibarrAccount'
-import MobytUserStatus from '~/models/Organization/MobytUserStatus'
 import UrlUtils from '~/services/utils/urlUtils'
 import { useDownloadFromRoute } from '~/composables/utils/useDownloadFromRoute'
 import { useApiLegacyRequestService } from '~/composables/data/useApiLegacyRequestService'
 import { usePageStore } from '~/stores/page'
 import { DOLIBARR_BILLING_DOC_TYPE } from '~/types/enum/enums'
-import type {AsyncDataRequestStatus} from "#app/composables/asyncData";
-import {FETCHING_STATUS} from "~/types/enum/data";
+import LayoutMobytStatus from '~/components/Layout/MobytStatus.vue'
 
 // meta
 definePageMeta({
@@ -516,7 +497,6 @@ const showDialogTrialStopConfirmation: Ref<boolean> = ref(false)
 const openedPanels: Ref<Array<string>> = initPanel()
 const organizationProfile = getOrganizationProfile()
 const accessProfileStore = useAccessProfileStore()
-const { mobytStatus, mobytPendingStatus } = getMobytInformations()
 
 const { data: dolibarrAccount, status: dolibarrStatus } = fetch(
   DolibarrAccount,
@@ -603,28 +583,6 @@ function getOrganizationProfile() {
   return organizationProfile
 }
 
-/**
- * Récupération des informations Mobyt
- */
-function getMobytInformations(): {
-  mobytStatus: Ref<MobytUserStatus | null>
-  mobytPendingStatus: Ref<boolean>
-} {
-  const mobytStatus: Ref<MobytUserStatus | null> = ref(null)
-  const mobytPendingStatus: Ref<AsyncDataRequestStatus> = ref(FETCHING_STATUS.PENDING)
-
-  if (ability.can('manage', 'texto')) {
-    const { data, status } = fetch(
-      MobytUserStatus,
-      organizationProfile!.id!,
-    ) as AsyncData<MobytUserStatus | null, Error | null>
-    mobytStatus.value = data
-    mobytPendingStatus.value = status
-  }
-
-  return { mobytStatus, mobytPendingStatus }
-}
-
 /**
  * Action lorsque l'on souhaite démarrer l'essai
  */

+ 15 - 3
plugins/init.server.ts

@@ -9,6 +9,7 @@ export default defineNuxtPlugin(async () => {
     return
   }
 
+  const runtimeConfig = useRuntimeConfig()
   const { redirectToLogout } = useRedirect()
 
   const bearer: CookieRef<string | null> = useCookie('BEARER') ?? null
@@ -17,13 +18,21 @@ export default defineNuxtPlugin(async () => {
     useCookie('SwitchAccessId') ?? null
 
   if (accessCookieId.value === null || Number.isNaN(accessCookieId.value)) {
-    redirectToLogout()
+    if (runtimeConfig.public.env === 'production') {
+      redirectToLogout()
+    } else {
+      console.error('Missing access id')
+    }
     return
   }
 
   const accessId: number = parseInt(accessCookieId.value)
   if (isNaN(accessId)) {
-    redirectToLogout()
+    if (runtimeConfig.public.env === 'production') {
+      redirectToLogout()
+    } else {
+      console.error('Invalid access id')
+    }
     return
   }
 
@@ -37,7 +46,10 @@ export default defineNuxtPlugin(async () => {
   try {
     await initiateProfile(accessId, bearer.value ?? '', switchId)
   } catch (error) {
-    if (error instanceof UnauthorizedError) {
+    if (
+      error instanceof UnauthorizedError &&
+      runtimeConfig.public.env === 'production'
+    ) {
       redirectToLogout()
     } else {
       throw error

+ 6 - 4
services/data/normalizer/hydraNormalizer.ts

@@ -24,21 +24,23 @@ class HydraNormalizer {
     const iriEncodedFields =
       Object.getPrototypeOf(entity).constructor.getIriEncodedFields()
 
+    const data = _.cloneDeep(entity)
+
     for (const field in iriEncodedFields) {
-      const value = entity[field]
+      const value = data[field]
       const targetEntity = iriEncodedFields[field].entity
 
       if (_.isArray(value)) {
-        entity[field] = value.map((id: number) => {
+        data[field] = value.map((id: number) => {
           return UrlUtils.makeIRI(targetEntity, id)
         })
       } else {
-        entity[field] =
+        data[field] =
           value !== null ? UrlUtils.makeIRI(targetEntity, value) : null
       }
     }
 
-    return entity.$toJson()
+    return data.$toJson()
   }
 
   /**

+ 10 - 2
services/layout/menuBuilder/abstractMenuBuilder.ts

@@ -86,6 +86,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
    * @param to
    * @param type
    * @param newTab
+   * @param endOfSubsection
    * @return {MenuItem}
    */
   protected createItem(
@@ -94,6 +95,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
     to: string = '',
     type: MENU_LINK_TYPE = MENU_LINK_TYPE.INTERNAL,
     newTab: boolean = false,
+    endOfSubsection: boolean = false,
   ): MenuItem {
     let url: string
     if (type === MENU_LINK_TYPE.INTERNAL) {
@@ -122,6 +124,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
       type,
       active: false,
       target: newTab ? LINK_TARGET.BLANK : LINK_TARGET.SELF,
+      endOfSubsection,
     }
   }
 
@@ -143,12 +146,16 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
    * @protected
    */
   protected makeChildren(
-    items: Array<{ pageName: string; icon?: string }>,
+    items: Array<{
+      pageName: string
+      icon?: string
+      endOfSubsection?: boolean
+    }>,
   ): MenuItems {
     const children: MenuItems = []
 
     items.forEach((item) => {
-      const { pageName, icon } = item
+      const { pageName, icon, endOfSubsection = false } = item
 
       if (this.ability.can('display', pageName)) {
         const to = this.router.resolve({ name: pageName })
@@ -161,6 +168,7 @@ abstract class AbstractMenuBuilder implements MenuBuilder {
           to: to.href,
           type: MENU_LINK_TYPE.INTERNAL,
           active: false,
+          endOfSubsection,
         })
       }
     })

+ 12 - 2
services/layout/menuBuilder/accountMenuBuilder.ts

@@ -132,11 +132,17 @@ export default class AccountMenuBuilder extends AbstractMenuBuilder {
           undefined,
           `/adherent_contacts/list/`,
           MENU_LINK_TYPE.V1,
+          false,
+          true,
         ),
       )
     }
 
-    children.push(...this.makeChildren([{ pageName: 'subscription_page' }]))
+    children.push(
+      ...this.makeChildren([
+        { pageName: 'subscription_page', endOfSubsection: true },
+      ]),
+    )
 
     if (this.ability.can('display', 'my_bills_page')) {
       children.push(
@@ -160,7 +166,11 @@ export default class AccountMenuBuilder extends AbstractMenuBuilder {
       )
     }
 
-    children.push(...this.makeChildren([{ pageName: 'my_settings_page' }]))
+    children.push(
+      ...this.makeChildren([
+        { pageName: 'my_settings_page', endOfSubsection: true },
+      ]),
+    )
 
     children.push(...this.makeChildren([{ pageName: 'freemium_organization_page' }]))
 

+ 81 - 67
services/layout/menuBuilder/configurationMenuBuilder.ts

@@ -15,6 +15,7 @@ export default class ConfigurationMenuBuilder extends AbstractMenuBuilder {
   build(): MenuItem | MenuGroup | null {
     const children: MenuItems = []
 
+    // 1. "Fiche de la structure" -> 'organization_page'
     if (this.ability.can('display', 'organization_page')) {
       children.push(
         this.createItem(
@@ -30,91 +31,79 @@ export default class ConfigurationMenuBuilder extends AbstractMenuBuilder {
       )
     }
 
-    // if (this.ability.can('display', 'cmf_licence_page')) {
-    //   children.push(
-    //     this.createItem(
-    //       'cmf_licence_generate',
-    //       undefined,
-    //       '/cmf_licence_structure',
-    //       MENU_LINK_TYPE.INTERNAL,
-    //     ),
-    //   )
-    // }
+    // 2. "Préférences" -> 'parameters_page'
+    children.push(
+      ...this.makeChildren([
+        { pageName: 'parameters_page', endOfSubsection: true },
+      ]),
+    )
 
-    if (this.ability.can('display', 'cmf_licence_page')) {
+    // 3. "Enseignements" -> 'education'
+    if (this.ability.can('display', 'education_page')) {
       children.push(
         this.createItem(
-          'cmf_licence_generate',
+          'education',
           undefined,
-          '/licence_cmf/organization',
+          '/educations/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    // if (this.ability.can('display', 'parameters_page')) {
-    //   children.push(
-    //     this.createItem(
-    //       'parameters',
-    //       undefined,
-    //       `/parameters`,
-    //       MENU_LINK_TYPE.INTERNAL,
-    //     ),
-    //   )
-    // }
-
-    children.push(...this.makeChildren([{ pageName: 'parameters_page' }]))
-
-    if (this.ability.can('display', 'place_page')) {
+    // 4. "Parcours" -> 'parcours'
+    if (this.ability.can('display', 'parcours_page')) {
       children.push(
         this.createItem(
-          'places',
+          'parcours',
           undefined,
-          '/places/list/',
+          '/education_curriculum_packs/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'education_page')) {
+    // 5. "Sections" -> 'activities'
+    if (this.ability.can('display', 'activities_page')) {
       children.push(
         this.createItem(
-          'education',
+          'activities',
           undefined,
-          '/educations/list/',
+          '/activities/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'tag_page')) {
-      children.push(
-        this.createItem('tags', undefined, '/taggs/list/', MENU_LINK_TYPE.V1),
-      )
-    }
-
-    if (this.ability.can('display', 'activities_page')) {
+    // 6. "Préinscription(s) en ligne" -> 'online_registration_settings'
+    if (this.ability.can('display', 'online_registration_settings_page')) {
       children.push(
         this.createItem(
-          'activities',
+          'online_registration_settings',
           undefined,
-          '/activities/list/',
+          UrlUtils.join(
+            '/main/edit/online_registration_settings/',
+            this.organizationProfile.id ?? '',
+          ),
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'template_systems_page')) {
+    // 7. "Dupliquer les cours hebdomadaires" -> 'course_duplication'
+    if (this.ability.can('display', 'course_duplication_page')) {
       children.push(
         this.createItem(
-          'template_systems',
+          'course_duplication',
           undefined,
-          '/template_systems/list/',
+          '/duplicate_courses',
           MENU_LINK_TYPE.V1,
+          false,
+          true,
         ),
       )
     }
 
+    // 8. "Facturation" -> 'billing_settings'
     if (this.ability.can('display', 'billing_settings_page')) {
       children.push(
         this.createItem(
@@ -129,76 +118,101 @@ export default class ConfigurationMenuBuilder extends AbstractMenuBuilder {
       )
     }
 
-    if (this.ability.can('display', 'online_registration_settings_page')) {
+    // 9. "Liste des produits" -> 'billing_product'
+    if (this.ability.can('display', 'billing_product_page')) {
       children.push(
         this.createItem(
-          'online_registration_settings',
+          'billing_product',
           undefined,
-          UrlUtils.join(
-            '/main/edit/online_registration_settings/',
-            this.organizationProfile.id ?? '',
-          ),
+          '/intangibles/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'course_duplication_page')) {
+    // 10. "Modèles de quotients familiaux" -> 'family_quotient_models'
+    if (this.ability.can('display', 'family_quotient_models_page')) {
       children.push(
         this.createItem(
-          'course_duplication',
+          'family_quotient_models',
           undefined,
-          '/duplicate_courses',
+          '/family_quotient_models/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'import_page')) {
+    // 11. "Echéanciers de facturation" -> 'billing_schedules'
+    if (this.ability.can('display', 'billing_schedules_settings_page')) {
       children.push(
-        this.createItem('import', undefined, '/import/all', MENU_LINK_TYPE.V1),
+        this.createItem(
+          'billing_schedules',
+          undefined,
+          '/bill_schedules/list/',
+          MENU_LINK_TYPE.V1,
+          false,
+          true,
+        ),
       )
     }
 
-    if (this.ability.can('display', 'parcours_page')) {
+    // 12. "Lieux" -> 'places'
+    if (this.ability.can('display', 'place_page')) {
       children.push(
         this.createItem(
-          'parcours',
+          'places',
           undefined,
-          '/family_quotient_models/list/',
+          '/places/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'family_quotient_models_page')) {
+    // 13. "Mails système" -> 'template_systems'
+    if (this.ability.can('display', 'template_systems_page')) {
       children.push(
         this.createItem(
-          'family_quotient_models',
+          'template_systems',
           undefined,
-          '/family_quotient_models/list/',
+          '/template_systems/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'billing_schedules_settings_page')) {
+    // 14. "Pseudonymisation" -> 'pseudonymization'
+    if (this.ability.can('display', 'pseudonymization_page')) {
       children.push(
         this.createItem(
-          'billing_schedules',
+          'pseudonymization',
           undefined,
-          '/bill_schedules/list/',
+          '/pseudonymizationList/list/',
           MENU_LINK_TYPE.V1,
         ),
       )
     }
 
-    if (this.ability.can('display', 'pseudonymization_page')) {
+    // 15. "Tags" -> 'tags'
+    if (this.ability.can('display', 'tag_page')) {
+      children.push(
+        this.createItem('tags', undefined, '/taggs/list/', MENU_LINK_TYPE.V1),
+      )
+    }
+
+    // 16. "Importer" -> 'import'
+    if (this.ability.can('display', 'import_page')) {
+      children.push(
+        this.createItem('import', undefined, '/import/all', MENU_LINK_TYPE.V1),
+      )
+    }
+
+    // CMF licence (not in the required order, but keeping it at the end)
+    if (this.ability.can('display', 'cmf_licence_page')) {
       children.push(
         this.createItem(
-          'pseudonymization',
+          'cmf_licence_generate',
           undefined,
-          '/pseudonymizationList/list/',
+          '/licence_cmf/organization',
           MENU_LINK_TYPE.V1,
         ),
       )

+ 5 - 0
stores/accessProfile.ts

@@ -160,6 +160,10 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     setFamilyAccesses(Array.from(profile.familyAccesses))
   }
 
+  const setActivityYear = (year: number) => {
+    activityYear.value = year
+  }
+
   const setHistorical = (past: boolean, present: boolean, future: boolean) => {
     historical.value = <Historical>{
       past,
@@ -213,6 +217,7 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     setFamilyAccesses,
     initiateProfile,
     setProfile,
+    setActivityYear,
     setHistorical,
     setHistoricalRange,
     preferencesId,

+ 7 - 1
stores/sse.ts

@@ -17,7 +17,13 @@ export const useSseStore = defineStore('sse', () => {
     switch (event.operation) {
       case 'update':
       case 'create':
-        em.save(instance, true)
+        if (model.entity === 'my_profile') {
+          const accessProfileStore = useAccessProfileStore()
+          accessProfileStore.initiateProfile(instance)
+        } else {
+          // Cas générique d'une entité standard
+          em.save(instance, true)
+        }
         break
 
       case 'delete':

+ 19 - 2
tests/units/services/layout/menuBuilder/abstractMenuBuilder.test.ts

@@ -110,7 +110,15 @@ describe('createItem', () => {
 
     const result = menuBuilder.createItem(label, icon, to, type)
 
-    expect(result).toEqual({ icon, label, to, target, type, active: false })
+    expect(result).toEqual({
+      icon,
+      label,
+      to,
+      target,
+      type,
+      active: false,
+      endOfSubsection: false,
+    })
   })
 
   test('default values', () => {
@@ -123,6 +131,7 @@ describe('createItem', () => {
       target: '_self',
       type: MENU_LINK_TYPE.INTERNAL,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -192,6 +201,7 @@ describe('makeChildren', () => {
         to: 'foo',
         type: 0,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })
@@ -207,7 +217,14 @@ describe('makeChildren', () => {
     ])
 
     expect(children).toEqual([
-      { label: 'foo_page', icon: undefined, to: 'foo', type: 0, active: false },
+      {
+        label: 'foo_page',
+        icon: undefined,
+        to: 'foo',
+        type: 0,
+        active: false,
+        endOfSubsection: false,
+      },
     ])
   })
   test('not allowed', () => {

+ 7 - 0
tests/units/services/layout/menuBuilder/accessMenuBuilder.test.ts

@@ -74,6 +74,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
 
     // @ts-ignore
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -118,6 +121,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -134,6 +138,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -150,6 +155,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -166,6 +172,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 117 - 92
tests/units/services/layout/menuBuilder/accountMenuBuilder.test.ts

@@ -61,16 +61,13 @@ describe('build', () => {
 
     // Has the logout action
     // @ts-ignore
-    expect(result.actions).toEqual([
-      {
-        label: 'logout',
-        icon: undefined,
-        to: 'https://mydomain.com/#/logout',
-        target: '_self',
-        type: MENU_LINK_TYPE.V1,
-        active: false,
-      },
-    ])
+    const logoutAction = result.actions[0]
+    expect(logoutAction).toHaveProperty('label', 'logout')
+    expect(logoutAction).toHaveProperty('icon', undefined)
+    expect(logoutAction).toHaveProperty('to', 'https://mydomain.com/#/logout')
+    expect(logoutAction).toHaveProperty('target', '_self')
+    expect(logoutAction).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(logoutAction).toHaveProperty('active', false)
   })
 
   test('has profile icon : mister)', () => {
@@ -108,16 +105,15 @@ describe('build', () => {
 
     // Still has the logout action
     // @ts-ignore
-    expect(group.actions).toEqual([
-      {
-        label: 'logout',
-        icon: undefined,
-        to: 'https://mydomain.com/#/logout',
-        target: '_self',
-        type: MENU_LINK_TYPE.V1,
-        active: false,
-      },
-    ])
+    expect(group.actions.length).toEqual(1)
+    // @ts-ignore
+    const logoutAction = group.actions[0]
+    expect(logoutAction).toHaveProperty('label', 'logout')
+    expect(logoutAction).toHaveProperty('icon', undefined)
+    expect(logoutAction).toHaveProperty('to', 'https://mydomain.com/#/logout')
+    expect(logoutAction).toHaveProperty('target', '_self')
+    expect(logoutAction).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(logoutAction).toHaveProperty('active', false)
   })
 
   test('has only rights for menu my_schedule_page', () => {
@@ -127,14 +123,14 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_schedule_page',
-      icon: undefined,
-      to: 'https://mydomain.com/#/my_calendar',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_schedule_page')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty('to', 'https://mydomain.com/#/my_calendar')
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu attendance_bookings_menu', () => {
@@ -144,14 +140,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'attendance_bookings_menu',
-      icon: undefined,
-      to: 'https://mydomain.com/#/own_attendance',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'attendance_bookings_menu')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/own_attendance',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_attendance', () => {
@@ -161,14 +160,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_attendance',
-      icon: undefined,
-      to: 'https://mydomain.com/#/my_attendances/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_attendance')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/my_attendances/list/',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_invitation', () => {
@@ -178,14 +180,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_invitation',
-      icon: undefined,
-      to: 'https://mydomain.com/#/my_invitations/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_invitation')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/my_invitations/list/',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_students', () => {
@@ -195,14 +200,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_students',
-      icon: undefined,
-      to: 'https://mydomain.com/#/my_students/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_students')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/my_students/list/',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_students_education_students', () => {
@@ -213,14 +221,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_students_education_students',
-      icon: undefined,
-      to: 'https://mydomain.com/#/my_students_education_students/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_students_education_students')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/my_students_education_students/list/',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_education_students', () => {
@@ -230,14 +241,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_education_students',
-      icon: undefined,
-      to: 'https://mydomain.com/#/main/my_profile/123/dashboard/my_education_students/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_education_students')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/main/my_profile/123/dashboard/my_education_students/list/',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu send_an_email', () => {
@@ -247,14 +261,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'send_an_email',
-      icon: undefined,
-      to: 'https://mydomain.com/#/list/create/emails',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'send_an_email')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/list/create/emails',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_documents', () => {
@@ -264,14 +281,17 @@ describe('build', () => {
     )
 
     // @ts-ignore
-    expect(menuBuilder.build().children[0]).toEqual({
-      label: 'my_documents',
-      icon: undefined,
-      to: 'https://mydomain.com/#/main/my_profile/123/dashboard/show/my_access_file',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const menuItem = menuBuilder.build().children[0]
+    expect(menuItem).toHaveProperty('label', 'my_documents')
+    expect(menuItem).toHaveProperty('icon', undefined)
+    expect(menuItem).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/main/my_profile/123/dashboard/show/my_access_file',
+    )
+    expect(menuItem).toHaveProperty('target', '_self')
+    expect(menuItem).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(menuItem).toHaveProperty('active', false)
+    expect(menuItem).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu my_profile', () => {
@@ -288,6 +308,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -305,6 +326,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: true,
     })
   })
 
@@ -325,6 +347,7 @@ describe('build', () => {
       to: 'subscription',
       type: MENU_LINK_TYPE.INTERNAL,
       active: false,
+      endOfSubsection: true,
     })
 
     expect(router.resolve).toHaveBeenCalledWith({ name: 'subscription_page' })
@@ -344,6 +367,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -361,6 +385,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 7 - 0
tests/units/services/layout/menuBuilder/admin2iosMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -118,6 +121,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -134,6 +138,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -150,6 +155,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -166,6 +172,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 2 - 0
tests/units/services/layout/menuBuilder/agendaMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 3 - 0
tests/units/services/layout/menuBuilder/basicomptaMenuBuilder.test.ts

@@ -62,6 +62,7 @@ describe('build', () => {
       type: MENU_LINK_TYPE.V1,
       target: '_self',
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -77,6 +78,7 @@ describe('build', () => {
       type: MENU_LINK_TYPE.V1,
       target: '_self',
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -92,6 +94,7 @@ describe('build', () => {
       type: MENU_LINK_TYPE.V1,
       target: '_self',
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 10 - 0
tests/units/services/layout/menuBuilder/billingMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -118,6 +121,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -134,6 +138,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -150,6 +155,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -166,6 +172,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -182,6 +189,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -198,6 +206,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -214,6 +223,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 3 - 0
tests/units/services/layout/menuBuilder/communicationMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 130 - 104
tests/units/services/layout/menuBuilder/configurationMenuBuilder.test.ts

@@ -52,7 +52,7 @@ describe('build', () => {
     expect(result.label).toEqual('configuration')
     expect(result.icon).toEqual({ name: 'fas fa-cogs' })
     // @ts-ignore
-    expect(result.children.length).toEqual(16)
+    expect(result.children.length).toEqual(17)
   })
 
   test('has no items', () => {
@@ -67,14 +67,17 @@ describe('build', () => {
     )
     organizationProfile.id = 123
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'organization_page',
-      icon: undefined,
-      to: 'https://mydomain.com/#/main/organizations/123/dashboard',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'organization_page')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/main/organizations/123/dashboard',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu cmf_licence_generate', () => {
@@ -83,14 +86,17 @@ describe('build', () => {
         action === 'display' && subject === 'cmf_licence_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'cmf_licence_generate',
-      icon: undefined,
-      to: 'https://mydomain.com/#/licence_cmf/organization',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'cmf_licence_generate')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/licence_cmf/organization',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
     //
     // expect(menuBuilder.build()).toEqual({
     //   label: 'cmf_licence_generate',
@@ -109,13 +115,12 @@ describe('build', () => {
 
     menuBuilder.organizationProfile.id = 123
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'parameters_page',
-      icon: undefined,
-      to: undefined,
-      type: MENU_LINK_TYPE.INTERNAL,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'parameters_page')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.INTERNAL)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', true)
 
     // expect(menuBuilder.build()).toEqual({
     //   label: 'parameters',
@@ -132,14 +137,14 @@ describe('build', () => {
         action === 'display' && subject === 'place_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'places',
-      icon: undefined,
-      to: 'https://mydomain.com/#/places/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'places')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty('to', 'https://mydomain.com/#/places/list/')
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu education', () => {
@@ -148,14 +153,17 @@ describe('build', () => {
         action === 'display' && subject === 'education_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'education',
-      icon: undefined,
-      to: 'https://mydomain.com/#/educations/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'education')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/educations/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu tag', () => {
@@ -164,14 +172,14 @@ describe('build', () => {
         action === 'display' && subject === 'tag_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'tags',
-      icon: undefined,
-      to: 'https://mydomain.com/#/taggs/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'tags')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty('to', 'https://mydomain.com/#/taggs/list/')
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu activities', () => {
@@ -180,14 +188,17 @@ describe('build', () => {
         action === 'display' && subject === 'activities_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'activities',
-      icon: undefined,
-      to: 'https://mydomain.com/#/activities/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'activities')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/activities/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu course_duplication', () => {
@@ -196,14 +207,17 @@ describe('build', () => {
         action === 'display' && subject === 'course_duplication_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'course_duplication',
-      icon: undefined,
-      to: 'https://mydomain.com/#/duplicate_courses',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'course_duplication')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/duplicate_courses',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', true)
   })
 
   test('has only rights for menu import', () => {
@@ -212,14 +226,14 @@ describe('build', () => {
         action === 'display' && subject === 'import_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'import',
-      icon: undefined,
-      to: 'https://mydomain.com/#/import/all',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'import')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty('to', 'https://mydomain.com/#/import/all')
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu parcours', () => {
@@ -228,14 +242,17 @@ describe('build', () => {
         action === 'display' && subject === 'parcours_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'parcours',
-      icon: undefined,
-      to: 'https://mydomain.com/#/family_quotient_models/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'parcours')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/education_curriculum_packs/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu family_quotient_models', () => {
@@ -244,14 +261,17 @@ describe('build', () => {
         action === 'display' && subject === 'family_quotient_models_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'family_quotient_models',
-      icon: undefined,
-      to: 'https://mydomain.com/#/family_quotient_models/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'family_quotient_models')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/family_quotient_models/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 
   test('has only rights for menu billing_schedules', () => {
@@ -260,14 +280,17 @@ describe('build', () => {
         action === 'display' && subject === 'billing_schedules_settings_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'billing_schedules',
-      icon: undefined,
-      to: 'https://mydomain.com/#/bill_schedules/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'billing_schedules')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/bill_schedules/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', true)
   })
 
   test('has only rights for menu pseudonymization', () => {
@@ -276,13 +299,16 @@ describe('build', () => {
         action === 'display' && subject === 'pseudonymization_page',
     )
 
-    expect(menuBuilder.build()).toEqual({
-      label: 'pseudonymization',
-      icon: undefined,
-      to: 'https://mydomain.com/#/pseudonymizationList/list/',
-      target: '_self',
-      type: MENU_LINK_TYPE.V1,
-      active: false,
-    })
+    const result = menuBuilder.build()
+    expect(result).toHaveProperty('label', 'pseudonymization')
+    expect(result).toHaveProperty('icon', undefined)
+    expect(result).toHaveProperty(
+      'to',
+      'https://mydomain.com/#/pseudonymizationList/list/',
+    )
+    expect(result).toHaveProperty('target', '_self')
+    expect(result).toHaveProperty('type', MENU_LINK_TYPE.V1)
+    expect(result).toHaveProperty('active', false)
+    expect(result).toHaveProperty('endOfSubsection', false)
   })
 })

+ 17 - 0
tests/units/services/layout/menuBuilder/cotisationsMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -118,6 +121,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -134,6 +138,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -150,6 +155,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -166,6 +172,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -182,6 +189,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -198,6 +206,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -214,6 +223,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -230,6 +240,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -246,6 +257,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -262,6 +274,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -278,6 +291,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -294,6 +308,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -310,6 +325,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -326,6 +342,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 1 - 0
tests/units/services/layout/menuBuilder/donorsMenuBuilder.test.ts

@@ -53,6 +53,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 

+ 6 - 0
tests/units/services/layout/menuBuilder/educationalMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -86,6 +87,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -102,6 +104,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -118,6 +121,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -134,6 +138,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -151,6 +156,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 1 - 0
tests/units/services/layout/menuBuilder/equipmentMenuBuilder.test.ts

@@ -53,6 +53,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 

+ 3 - 0
tests/units/services/layout/menuBuilder/myAccessesMenuBuilder.test.ts

@@ -65,6 +65,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'Séraphin',
@@ -73,6 +74,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'Lilou',
@@ -81,6 +83,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })

+ 4 - 0
tests/units/services/layout/menuBuilder/myFamilyMenuBuilder.test.ts

@@ -96,6 +96,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'Dupuis Séraphin',
@@ -104,6 +105,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'Dubois Lilou',
@@ -112,6 +114,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'Soprano Tony',
@@ -120,6 +123,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })

+ 4 - 0
tests/units/services/layout/menuBuilder/statsMenuBuilder.test.ts

@@ -70,6 +70,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 
@@ -89,6 +90,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'accesses_quotas_courses_hebdos',
@@ -97,6 +99,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.V1,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })
@@ -114,6 +117,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.V1,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 1 - 0
tests/units/services/layout/menuBuilder/websiteAdminMenuBuilder.test.ts

@@ -73,6 +73,7 @@ describe('build', () => {
       target: '_self',
       type: MENU_LINK_TYPE.EXTERNAL,
       active: false,
+      endOfSubsection: false,
     })
   })
 })

+ 7 - 0
tests/units/services/layout/menuBuilder/websiteListMenuBuilder.test.ts

@@ -61,6 +61,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })
@@ -84,6 +85,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'parent2',
@@ -92,6 +94,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'parent3',
@@ -100,6 +103,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })
@@ -125,6 +129,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'parent1',
@@ -133,6 +138,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
       {
         label: 'parent2',
@@ -141,6 +147,7 @@ describe('build', () => {
         target: '_self',
         type: MENU_LINK_TYPE.EXTERNAL,
         active: false,
+        endOfSubsection: false,
       },
     ])
   })

+ 2 - 0
types/layout.d.ts

@@ -21,6 +21,8 @@ interface MenuItem {
   active: boolean
   /** Définit l'attribut 'target' du lien */
   target?: LINK_TARGET
+  /** Indique si l'item est à la fin d'une sous-section (bordure basse plus épaisse) */
+  endOfSubsection?: boolean
 }
 
 /**