Browse Source

Merge branch 'feature/subdomains' into develop

Olivier Massot 3 năm trước cách đây
mục cha
commit
44fe1a7dc2

+ 96 - 0
components/Form/Parameters/Subdomains.vue

@@ -0,0 +1,96 @@
+<!-- Liste des sous-domaines -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-container class="container">
+        <div>
+          <span>{{ $t('yourSubdomains') }} : </span>
+        </div>
+        <v-skeleton-loader
+          v-if="fetchState.pending"
+          type="image"
+        />
+        <div v-else>
+          <v-simple-table class="my-2">
+            <template v-slot:default>
+              <tbody>
+                <tr
+                  v-for="subdomain in subdomains"
+                  :key="subdomain.id"
+                  class="subdomainItem"
+                  @click="goToEditPage(subdomain.id)"
+                >
+                    <td>{{ subdomain.subdomain }}</td>
+                    <td>
+                      <span v-if="subdomain.active">
+                        <v-icon class="ot_green--text icon">
+                          fa-solid fa-check
+                        </v-icon> {{ $t('active') }}
+                      </span>
+                    </td>
+                </tr>
+              </tbody>
+            </template>
+          </v-simple-table>
+          <v-btn
+            v-if="subdomains.length < 3"
+            class="ot_white--text ot_green float-left"
+          >
+            <NuxtLink to="/parameters/subdomain/new" class="no-decoration">
+              <v-icon class="mr-1">fa-plus-circle</v-icon>
+              <span>{{$t('add')}}</span>
+            </NuxtLink>
+          </v-btn>
+        </div>
+      </v-container>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, ref, Ref, useContext, useRouter} from '@nuxtjs/composition-api'
+import { Repository as VuexRepository } from '@vuex-orm/core/dist/src/repository/Repository'
+import {Model, Query} from '@vuex-orm/core'
+import { repositoryHelper } from '~/services/store/repository'
+import {Subdomain} from "~/models/Organization/Subdomain";
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Collection} from "@vuex-orm/core/dist/src/data/Data";
+import {queryHelper} from "~/services/store/query";
+import {Organization} from "~/models/Organization/Organization";
+
+export default defineComponent({
+  setup () {
+    const { store, $dataProvider } = useContext()
+    const { getCollection } = useDataUtils($dataProvider)
+    const router = useRouter();
+
+    const organizationId: number = store.state.profile.organization.id
+    const { fetchState } = getCollection(Subdomain, Organization, organizationId)
+
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(Subdomain)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+    const subdomains: ComputedRef<Collection> = computed(() => queryHelper.getCollection(query.value))
+    const goToEditPage = function(id: number) {
+      router.push('/parameters/subdomain/' + id)
+    }
+
+    return {
+      subdomains,
+      fetchState,
+      goToEditPage
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Subdomain)
+  }
+})
+
+</script>
+<style scoped>
+  .subdomainItem {
+    cursor: pointer;
+  }
+  .subdomainItem .icon {
+    font-size: 12px;
+  }
+</style>

+ 7 - 2
components/Ui/Form.vue

@@ -120,8 +120,12 @@ export default defineComponent({
       repositoryHelper.updateStoreFromField(props.model, entry.value, newValue, field)
     }
 
+    const validate = function () {
+      return form.value.validate()
+    }
+
     const submit = async (next: string|null = null) => {
-      if(form.value.validate()){
+      if(await validate()){
         markAsNotDirty()
 
         try {
@@ -202,7 +206,8 @@ export default defineComponent({
       quitForm,
       closeDialog,
       saveAndQuit,
-      actions
+      actions,
+      validate
     }
   }
 })

+ 16 - 15
components/Ui/Input/Text.vue

@@ -5,20 +5,21 @@ Champs de saisie de texte
 -->
 
 <template>
-    <v-text-field
-      autocomplete="off"
-      :value="data"
-      :label="$t(label_field)"
-      :rules="rules"
-      :disabled="readonly"
-      :type="type === 'password' ? (show ? 'text' : type) : type"
-      :error="error || !!violation"
-      :error-messages="errorMessage || violation ? $t(violation) : ''"
-      @change="onChange($event)"
-      v-mask="mask"
-      :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
-      @click:append="show = !show"
-    />
+  <v-text-field
+    autocomplete="off"
+    :value="data"
+    :label="$t(label_field)"
+    :rules="rules"
+    :disabled="readonly"
+    :type="type === 'password' ? (show ? 'text' : type) : type"
+    :error="error || !!violation"
+    :error-messages="errorMessage || (violation ? $t(violation) : '')"
+    @change="onChange($event); $emit('change', $event)"
+    @input="$emit('input', $event, field)"
+    v-mask="mask"
+    :append-icon="type === 'password' ? (show ? 'mdi-eye' : 'mdi-eye-off') : ''"
+    @click:append="show = !show"
+  />
 </template>
 
 <script lang="ts">
@@ -93,7 +94,7 @@ export default defineComponent({
 })
 </script>
 
-<style>
+<style scoped>
   input:read-only{
     color: #666 !important;
   }

+ 27 - 1
composables/form/useValidator.ts

@@ -38,7 +38,33 @@ export function useValidator($dataProvider: DataProvider, i18n: VueI18n) {
     }
   }
 
+  function useHandleSubdomain() {
+
+    const checkSubdomainAvailability = async (subdomain: string | null): Promise<boolean> => {
+      if (subdomain === null) {
+        return true
+      }
+
+      const response = await $dataProvider.invoke({
+        type: QUERY_TYPE.DEFAULT,
+        url: '/api/subdomains',
+        listArgs: {
+          filters:[
+            {key: 'subdomain', value: subdomain}
+          ]
+        }
+
+      })
+
+      return typeof response !== 'undefined' && response.metadata.totalItems === 0
+    }
+    return {
+      checkSubdomainAvailability
+    }
+  }
+
   return {
-    useHandleSiret
+    useHandleSiret,
+    useHandleSubdomain
   }
 }

+ 1 - 1
composables/layout/Menus/websiteMenu.ts

@@ -53,7 +53,7 @@ class WebsiteMenu extends BaseMenu implements Menu {
   }
 
   getWebsite (organization: organizationState): string {
-    return organization.website ? organization.website : this.$config.baseURL_typo3.replace('###subDomain###', organization.subDomain)
+    return organization.website ?? '';
   }
 }
 

+ 14 - 0
lang/content/parameters/fr-FR.js

@@ -1,5 +1,19 @@
 export default (context, locale) => {
   return ({
     'help_super_admin': 'Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise surtout pour la gestion de votre site internet et, à la première connexion au logiciel, afin de créer des comptes pour tous membres de votre structure. Enfin, il peut également être utile en cas de dépannage dans certaines situations particulières.',
+    yourWebsiteAddressIs: 'L\'adresse de votre site web est',
+    areYourSureYouWantToDisableYourOpentalentWebsite: 'Êtes-vous sûr(e) de vouloir désactiver votre site web Opentalent',
+    youRegisteredTheFollowingSubdomain: 'Vous avez enregistré le sous-domaine suivant',
+    subdomainIsCurrentlyActive: 'Le sous-domaine est actuellement actif',
+    doYouWantToActivateThisSubdomain: 'Voulez-vous activer ce sous-domaine',
+    activate: 'Activer',
+    active: 'Actif',
+    pleaseEnterYourNewSubdomain: 'Veuillez saisir votre nouveau sous-domaine',
+    subdomain_need_to_have_0_to_60_cars: 'Le sous-domaine doit comporter de 2 à 60 caractères',
+    this_subdomain_is_already_in_use: 'Ce sous-domaine est déjà utilisé',
+    this_subdomain_is_available: 'Ce sous-domaine est disponible',
+    subdomain_can_not_contain_spaces_or_special_cars: 'Le sous-domaine ne doit pas contenir d\'espaces ni de caractères spéciaux',
+    please_enter_a_value_for_the_subdomain: 'Veuillez saisir une valeur pour le sous-domaine',
+    validation_ongoing: 'Validation en cours',
   })
 }

+ 4 - 3
lang/field/fr-FR.js

@@ -8,6 +8,7 @@ export default (context, locale) => {
     username: 'Login de connexion',
     residenceArea: 'Zones de résidence',
     desactivateOpentalentSiteWeb: 'Désactiver le site opentalent',
+    reactivateOpentalentSiteWeb: 'Réactiver le site Opentalent',
     passwordSMS: 'Mot de passe SMS',
     usernameSMS: 'Nom d\'utilisateur SMS',
     smsSenderName: 'Personnaliser le nom de l\'expéditeur SMS',
@@ -34,10 +35,9 @@ export default (context, locale) => {
     editCriteriaNotationByAdminOnly: 'Autoriser uniquement l\'administration à modifier les critères d\'évaluation',
     trackingValidation: 'Contrôle et validation du suivi pédagogique par l\'administration',
     publicationDirectors: 'Directeur(s) de publication',
-    website: 'Autre site web',
+    otherWebsite: 'Autre site web',
     newSubDomain: 'Nouveau sous domaine',
-    subDomainHistorical: 'Historique de vos sous domaine(s)',
-    otherWebsite: 'Votre site Opentalent est',
+    yourSubdomains: 'Vos sous-domaines',
     timezone: 'Fuseau horaire',
     qrCode: 'QrCode pour la licence',
     studentsAreAdherents: 'Les élèves sont également adhérents de l\'association',
@@ -130,5 +130,6 @@ export default (context, locale) => {
     contactpoint_type: 'Type de contact',
     phoneNumberInvalid: 'Numéro de téléphone invalide',
     logo: 'Logo',
+    subdomain: 'Sous-domaine',
   })
 }

+ 4 - 0
lang/layout/fr-FR.js

@@ -210,5 +210,9 @@ export default (context, locale) => {
     your_message: "Votre message",
     has_been_sent: "a été envoyé",
     ready_to_be: "est prêt à être",
+    none: "Aucun",
+    please_confirm: "Veuillez confirmer",
+    yes: "Oui",
+    no: "Non",
   })
 }

+ 1 - 0
lang/rulesAndErrors/fr-FR.js

@@ -19,5 +19,6 @@ export default (context, locale) => {
     PRINCIPAL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact principaux',
     BILL_non_unique: 'Vous ne pouvez pas avoir 2 points de contact de facturation',
     CONTACT_non_unique: 'Vous ne pouvez pas avoir 2 points de contact',
+    could_not_contact_server_please_try_again: 'Le serveur n\'a pas pu être contacté, veuillez réessayer un peu plus tard',
   })
 }

+ 1 - 1
layouts/error.vue

@@ -19,7 +19,7 @@ export default defineComponent({
   setup (props) {
     const {$config} = useContext()
     const baseLegacyUrl:string = $config.baseURL_adminLegacy
-    if(process.client && props.error.statusCode === 404)
+    if(process.client && props.error.statusCode === 404 && process.env.NODE_ENV === 'production')
       window.location.href= `${baseLegacyUrl}/dashboard`
 
     return {

+ 6 - 6
models/Organization/Parameters.ts

@@ -1,4 +1,4 @@
-import {Str, Model, Uid, Bool, Num, Attr} from '@vuex-orm/core'
+import {Str, Model, Uid, Bool, Num, Attr, HasOne} from '@vuex-orm/core'
 
 export class Parameters extends Model {
   static entity = 'parameters'
@@ -34,13 +34,10 @@ export class Parameters extends Model {
   logoDonorsMove!: boolean
 
   @Str(null, { nullable: true })
-  subDomain!: string|null
-
-  @Str(null, { nullable: true })
-  website!: string|null
+  otherWebsite!: string|null
 
   @Str(null, { nullable: true })
-  otherWebsite!: string|null
+  customDomain!: string|null
 
   @Bool(false, { nullable: false })
   desactivateOpentalentSiteWeb!: boolean
@@ -113,4 +110,7 @@ export class Parameters extends Model {
 
   @Bool(false, { nullable: false })
   sendAttendanceSms!: boolean
+
+  @Attr([])
+  subdomains!: []
 }

+ 15 - 0
models/Organization/Subdomain.ts

@@ -0,0 +1,15 @@
+import {Bool, Model, Str, Uid} from "@vuex-orm/core";
+
+
+export class Subdomain extends Model {
+  static entity = 'subdomains'
+
+  @Uid()
+  id!: number | string | null
+
+  @Str(null, {nullable: true})
+  subdomain!: string | null
+
+  @Bool(false, { nullable: false })
+  active!: boolean
+}

+ 103 - 21
pages/parameters/communication.vue

@@ -9,30 +9,63 @@
               <v-row>
                 <v-col cols="12" sm="6">
                   <div>
-                    <span>{{ $t('otherWebsite') }} : </span>
-                    <span>{{ entry['otherWebsite'] }}</span>
+                    <span>{{ $t('yourWebsiteAddressIs') }} : </span>
+                    <span>{{ getCurrentWebsite(entry) || $t('none') }}</span>
                   </div>
                 </v-col>
 
-                <v-col cols="12" sm="6">
-                  <div>
-                    <span>{{ $t('subDomainHistorical') }} : </span>
-                  </div>
-                </v-col>
-
-                <v-col cols="12" sm="6">
-                  <UiInputText field="newSubDomain" :data="entry['newSubDomain']" />
+                <v-col cols="12" sm="6" v-if="!organizationProfile.isCmf()">
+                  <v-btn
+                    color="error"
+                    v-if="entry['desactivateOpentalentSiteWeb'] === false"
+                    @click="confirmWebsiteDeactivation()"
+                  >{{ $t('desactivateOpentalentSiteWeb') }} </v-btn>
+
+                  <v-btn
+                    color="primary"
+                    v-else
+                    @click="reactivateOpentalentSiteWeb()"
+                  >{{ $t('reactivateOpentalentSiteWeb') }}</v-btn>
+
+                  <lazy-LayoutDialog
+                    :show="showSiteWebConfirmationDialog"
+                  >
+                    <template #dialogTitle>{{ $t('please_confirm')}}</template>
+                    <template #dialogText>
+                      <div class="ma-2">
+                        {{ $t('areYourSureYouWantToDisableYourOpentalentWebsite')}} ?
+                      </div>
+                    </template>
+                    <template #dialogBtn>
+                      <v-btn
+                        color="ot_super_light_grey"
+                        @click="showSiteWebConfirmationDialog=false"
+                      >
+                        {{ $t('cancel') }}
+                      </v-btn>
+                      <v-btn
+                        color="primary"
+                        @click="showSiteWebConfirmationDialog=false;desactivateOpentalentSiteWeb()"
+                      >
+                        {{ $t('yes') }}
+                      </v-btn>
+                    </template>
+                  </lazy-LayoutDialog>
                 </v-col>
 
-                <v-col cols="12" sm="6" v-if="!organizationProfile.isCmf()">
-                  <UiInputCheckbox field="desactivateOpentalentSiteWeb" :data="entry['desactivateOpentalentSiteWeb']" @update="updateRepository" />
+                <v-col
+                  cols="12"
+                  sm="6"
+                  v-if="entry['desactivateOpentalentSiteWeb'] === false"
+                >
+                  <FormParametersSubdomains></FormParametersSubdomains>
                 </v-col>
 
                 <v-col cols="12" sm="6">
-                  <UiInputText field="website" :data="entry['website']" @update="updateRepository" />
+                  <UiInputText field="otherWebsite" :data="entry['otherWebsite']" @update="updateRepository" />
                 </v-col>
 
-                <v-col cols="12" sm="6">
+                <v-col cols="12" sm="6" v-if="entry['desactivateOpentalentSiteWeb'] === false">
                   <UiInputAutocompleteWithAPI
                     field="publicationDirectors"
                     label="publicationDirectors"
@@ -77,23 +110,27 @@
 </template>
 
 <script lang="ts">
-import {computed, ComputedRef, defineComponent, reactive, ref, useContext} from '@nuxtjs/composition-api'
-import { Organization } from '@/models/Organization/Organization'
-import { repositoryHelper } from '~/services/store/repository'
+import {computed, ComputedRef, defineComponent, reactive, ref, Ref, useContext} from '@nuxtjs/composition-api'
 import {useDataUtils} from "~/composables/data/useDataUtils";
 import {Parameters} from "~/models/Organization/Parameters";
-import {Query} from "@vuex-orm/core";
 import {$organizationProfile} from "~/services/profile/organizationProfile";
-import ModelsUtils from "~/services/utils/modelsUtils";
 import {useAccessesProvider} from "~/composables/data/useAccessesProvider";
+import {repositoryHelper} from "~/services/store/repository";
+import {Query} from "@vuex-orm/core";
+import UrlBuilder from "~/services/connection/urlBuilder";
+import {Subdomain} from "~/models/Organization/Subdomain";
+import {AnyJson} from "~/types/interfaces";
+import {queryHelper} from "~/services/store/query";
 
 export default defineComponent({
-  name: 'parameters',
+  name: 'communication',
   setup () {
     const {store, $dataProvider, app: {i18n}} = useContext()
     const {getItemToEdit} = useDataUtils($dataProvider)
     const {getPhysicalByFullName: accessSearch} = useAccessesProvider($dataProvider)
 
+    const showSiteWebConfirmationDialog: Ref<boolean> = ref(false);
+
     const organizationProfile = reactive($organizationProfile(store))
 
     const id = store.state.profile.organization.parametersId
@@ -102,6 +139,45 @@ export default defineComponent({
     const repository = repositoryHelper.getRepository(Parameters)
     const query: ComputedRef<Query> = computed(() => repository.query())
 
+    const entry: ComputedRef<AnyJson> = computed(() => {
+      return queryHelper.getFlattenEntry(query.value, id)
+    })
+
+    /**
+     * Build the URL of the current website of the organization
+     * Anywhere else, you can rely on organizationProfile.getWebsite(), but here this url has to be
+     * dynamic.
+     *
+     * @see https://ressources.opentalent.fr/display/SPEC/Preferences#Preferences-Siteinternet
+     *
+     * @param parameters
+     */
+    const getCurrentWebsite = function (parameters: Parameters) {
+      if (parameters.desactivateOpentalentSiteWeb) {
+        if (parameters.otherWebsite) {
+          return UrlBuilder.prependHttps(parameters.otherWebsite)
+        }
+        return null
+      }
+      if (parameters.customDomain) {
+        return UrlBuilder.prependHttps(parameters.customDomain)
+      }
+      // A ce niveau, tous les attributs de Parameters qui pourraient influer sur l'url du site ont été testés, les
+      // sous-domaines étant gérés sur d'autres écrans dédiés.
+      // On peut donc se reposer sur le profil de l'organisation.
+      return organizationProfile.getWebsite()
+    }
+
+    const confirmWebsiteDeactivation = function () {
+      showSiteWebConfirmationDialog.value = true
+    }
+    const desactivateOpentalentSiteWeb = function() {
+      repositoryHelper.updateStoreFromField(Parameters, entry.value, '1', 'desactivateOpentalentSiteWeb')
+    }
+    const reactivateOpentalentSiteWeb = function() {
+      repositoryHelper.updateStoreFromField(Parameters, entry.value, '0', 'desactivateOpentalentSiteWeb')
+    }
+
     return {
       query: () => query.value,
       rules: () => getRules(i18n),
@@ -109,7 +185,13 @@ export default defineComponent({
       id,
       fetchState,
       accessSearch,
-      model: Parameters
+      model: Parameters,
+      getCurrentWebsite,
+      confirmWebsiteDeactivation,
+      desactivateOpentalentSiteWeb,
+      showSiteWebConfirmationDialog,
+      reactivateOpentalentSiteWeb,
+      UrlBuilder
     }
   }
 })

+ 104 - 0
pages/parameters/subdomain/_id.vue

@@ -0,0 +1,104 @@
+<!-- Page de détails d'un sous-domaine -->
+<template>
+  <main>
+    <LayoutContainer>
+      <v-card class="mb-5 mt-4">
+        <FormToolbar title="subdomain" icon="fa-at"/>
+
+        <v-container fluid class="container pa-6">
+          <v-row>
+            <v-col cols="12" sm="12">
+              <v-skeleton-loader
+                v-if="fetchState.pending"
+                type="text"
+              />
+              <div v-else>
+                <div>
+                  {{ $t('youRegisteredTheFollowingSubdomain')}} :
+                </div>
+
+                <div class="pa-8">
+                  <b>{{ entry.subdomain }}</b> <span class="grey--text">.opentalent.fr</span>
+                </div>
+
+                <div>
+                  <div v-if="entry.active">
+                    <v-icon class="ot_green--text icon small mr-2">
+                      fa-solid fa-check
+                    </v-icon>
+                    {{ $t('subdomainIsCurrentlyActive') }}
+                  </div>
+                  <div v-else>
+                    {{ $t('doYouWantToActivateThisSubdomain') }} ?
+                  </div>
+                </div>
+
+                <div class="mt-6 d-flex flex-row">
+                  <v-btn to="/parameters/communication" class="mr-12">{{ $t('back') }}</v-btn>
+                  <div v-if="!entry.active">
+                    <v-btn color="primary" @click="activateAndQuit(entry)">{{ $t('activate') }}</v-btn>
+                  </div>
+                </div>
+
+              </div>
+            </v-col>
+          </v-row>
+        </v-container>
+      </v-card>
+    </LayoutContainer>
+  </main>
+</template>
+
+<script lang="ts">
+import {computed, ComputedRef, defineComponent, useContext, useRouter} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Subdomain} from "~/models/Organization/Subdomain";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import {Model, Query} from "@vuex-orm/core";
+import {repositoryHelper} from "~/services/store/repository";
+import {AnyJson} from "~/types/interfaces";
+import {queryHelper} from "~/services/store/query";
+import {QUERY_TYPE} from "~/types/enums";
+
+export default defineComponent({
+  name: 'EditFormParametersSubdomain',
+  setup () {
+    const {$dataProvider, $dataPersister, route} = useContext()
+    const {getItemToEdit} = useDataUtils($dataProvider)
+    const id = parseInt(route.value.params.id)
+    const {fetchState} = getItemToEdit(id, Subdomain)
+    const router = useRouter();
+
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(Subdomain)
+    const query: ComputedRef<Query> = computed(() => repository.query())
+    const entry: ComputedRef<AnyJson> = computed(() => {
+      return queryHelper.getFlattenEntry(query.value, id)
+    })
+
+    const submit = async () => {
+      await $dataPersister.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: Subdomain,
+        id: id,
+        query: query.value
+      })
+    }
+
+    const activateAndQuit = async function() {
+      repositoryHelper.updateStoreFromField(Subdomain, entry.value, true, 'active')
+      await submit()
+      router.push('/parameters/communication')
+    }
+
+    return {
+      id,
+      fetchState,
+      entry,
+      activateAndQuit
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Subdomain)
+  }
+})
+</script>

+ 137 - 0
pages/parameters/subdomain/new.vue

@@ -0,0 +1,137 @@
+<!-- Page de création d'un nouveau sous-domaine -->
+<template>
+  <main>
+    <v-skeleton-loader
+      v-if="loading"
+      type="text"
+    />
+    <div v-else>
+      <LayoutContainer>
+        <v-card class="mb-5 mt-4">
+          <FormToolbar title="subdomain" icon="fa-at"/>
+
+          <UiForm
+            :id="item.id"
+            ref="form"
+            :model="model"
+            :query="query()"
+            :submitActions="submitActions"
+          >
+            <template #form.input="{entry, updateRepository}">
+              <v-container fluid class="container">
+                <v-row>
+                  <v-col cols="12" sm="6">
+                    <div>{{ $t('pleaseEnterYourNewSubdomain')}} :</div>
+                  </v-col>
+                </v-row>
+                <v-row>
+                  <v-col cols="12" sm="6">
+                    <UiInputText
+                      field="subdomain"
+                      label="subdomain"
+                      :data="entry['subdomain']"
+                      type="string"
+                      :rules="rules()"
+                      @change="checkSubdomainHook($event, updateRepository)"
+                    />
+                  </v-col>
+                </v-row>
+                <i v-if="validationPending" class="validation_status">{{ $t('validation_ongoing') }}</i>
+                <i v-else-if="subdomainAvailable === true" class="validation_status valid">{{ $t('this_subdomain_is_available') }}</i>
+              </v-container>
+            </template>
+
+            <template #form.button>
+              <NuxtLink :to="{ path: '/parameters/communication'}" class="no-decoration">
+                <v-btn class="mr-4 ot_light_grey ot_grey--text">
+                  {{ $t('back') }}
+                </v-btn>
+              </NuxtLink>
+            </template>
+          </UiForm>
+        </v-card>
+      </LayoutContainer>
+    </div>
+  </main>
+</template>
+
+<script lang="ts">
+import {computed, defineComponent, ref, Ref, useContext} from '@nuxtjs/composition-api'
+import {useDataUtils} from "~/composables/data/useDataUtils";
+import {Subdomain} from "~/models/Organization/Subdomain";
+import {Repository as VuexRepository} from "@vuex-orm/core/dist/src/repository/Repository";
+import {Model, Query} from "@vuex-orm/core";
+import {repositoryHelper} from "~/services/store/repository";
+import {useValidator} from "~/composables/form/useValidator";
+import {AnyJson} from "~/types/interfaces";
+import {SUBMIT_TYPE} from "~/types/enums";
+
+export default defineComponent({
+  name: 'NewFormParametersSubdomain',
+  setup () {
+    const subdomainAvailable: Ref<boolean | null> = ref(null)
+    const form: Ref<HTMLCanvasElement | null> = ref(null);
+    const validationPending: Ref<boolean> = ref(false)
+
+    const {$dataProvider, store, app:{i18n}} = useContext()
+    const repository: VuexRepository<Model> = repositoryHelper.getRepository(Subdomain)
+    const query: Query = repository.query()
+    const { checkSubdomainAvailability } = useValidator($dataProvider, i18n).useHandleSubdomain()
+    const {createItem} = useDataUtils($dataProvider)
+    const {create, loading, item} = createItem(store, Subdomain)
+
+    const submitActions = computed(() => {
+      let actions:AnyJson = {}
+      actions[SUBMIT_TYPE.SAVE_AND_BACK] = { path: `/parameters/communication` }
+      return actions
+    })
+
+    const checkSubdomainHook = async (subdomain: string | null, updateRepository: any) => {
+      subdomainAvailable.value = null
+      if (subdomain !== null && subdomain.match(/^[\w\-]{2,60}$/) !== null) {
+        validationPending.value = true
+        subdomainAvailable.value = await checkSubdomainAvailability(subdomain);
+        validationPending.value = false
+      }
+      updateRepository(subdomain, 'subdomain');
+      (form.value as any).validate()
+    }
+
+    if(process.client){
+      const itemToCreate: Subdomain = new Subdomain()
+      create(itemToCreate)
+    }
+
+    /** todo Computed properties needs to be returned as functions until nuxt3 : https://github.com/nuxt-community/composition-api/issues/207 **/
+    return {
+      loading,
+      item,
+      model: Subdomain,
+      query: () => query,
+      submitActions,
+      form,
+      checkSubdomainHook,
+      validationPending,
+      subdomainAvailable,
+      rules: () => [
+        (subdomain: string | null) => (subdomain !== null) || i18n.t('please_enter_a_value_for_the_subdomain'),
+        (subdomain: string | null) => (subdomain !== null && subdomain.length >= 2 && subdomain.length <=60) || i18n.t('subdomain_need_to_have_0_to_60_cars'),
+        (subdomain: string | null) => (subdomain !== null && subdomain.match(/^[\w\-]+$/) !== null) || i18n.t('subdomain_can_not_contain_spaces_or_special_cars'),
+        () => (subdomainAvailable.value !== false) || i18n.t('this_subdomain_is_already_in_use')
+      ]
+    }
+  },
+  beforeDestroy() {
+    repositoryHelper.cleanRepository(Subdomain)
+  }
+})
+</script>
+
+<style scoped>
+.validation_status {
+  font-size: 13px;
+}
+.validation_status.valid {
+  color: green;
+}
+</style>

+ 12 - 1
services/connection/urlBuilder.ts

@@ -109,7 +109,6 @@ class UrlBuilder {
 
   /**
    * Construction d'une URL "image" qui ira concaténer l'id de l'image à downloeader passé en paramètre avec la ROOT Url définie
-   * @param {number} id
    * @param {ImageArgs} imgArgs
    * @param {string} baseUrl
    * @return {string}
@@ -141,6 +140,18 @@ class UrlBuilder {
     })
     return url
   }
+
+  /**
+   * Prepend the 'https://' part if neither 'http://' of 'https://' is present, else: does nothing
+   *
+   * @param url
+   */
+  public static prependHttps (url: string): string {
+    if (!url.match(/^https?:\/\/.*/)) {
+      url = 'https://' + url;
+    }
+    return url;
+  }
 }
 
 export default UrlBuilder

+ 3 - 2
services/data/baseDataManager.ts

@@ -40,9 +40,10 @@ abstract class BaseDataManager extends Hookable implements DataManager {
     const queryArguments = { ...this.defaultArguments, ...args }
     BaseDataManager.startLoading(queryArguments)
 
-    await this.triggerHooks(queryArguments)
+    const response = await this._invoke(queryArguments)
 
-    return await this._invoke(queryArguments)
+    await this.triggerHooks(queryArguments)
+    return response
   }
 
   /**

+ 5 - 0
services/data/dataPersister.ts

@@ -61,6 +61,11 @@ class DataPersister extends BaseDataManager {
     }
     return await dataProvider.process(deserializedResponse, dataProviderArgs)
   }
+
+  public async fetchProfile() {
+    console.log('refresh the current profile with updated data')
+    await this.ctx.store.dispatch('refreshUserProfile')
+  }
 }
 
 export default DataPersister

+ 2 - 2
services/data/hookable.ts

@@ -5,7 +5,7 @@ import BaseHook from '~/services/data/hooks/baseHook'
  * Base class for an object which support hooks
  */
 abstract class Hookable {
-  protected hooks: Array<typeof BaseHook> = []; // how could we replace 'any'?
+  protected hooks: Array<typeof BaseHook> = [];
 
   /**
    * Iterate over the available hooks and invoke the ones
@@ -14,7 +14,7 @@ abstract class Hookable {
   protected async triggerHooks (args: UrlArgs) {
     for (const Hook of this.sortedHooks()) {
       if (Hook.support(args)) {
-        await new Hook().invoke(args)
+        await new Hook(this).invoke(args)  // TODO: trouver une alternative au passage du this dans le constructeur, je trouve pas pour le moment :(
       }
     }
   }

+ 6 - 0
services/data/hooks/baseHook.ts

@@ -1,7 +1,13 @@
 import { DataProviderArgs } from '~/types/interfaces'
+import Hookable from "~/services/data/hookable";
 
 abstract class BaseHook {
   public static priority = 255
+  protected parent: Hookable;
+
+  constructor(parent: Hookable) {
+    this.parent = parent
+  }
 
   // eslint-disable-next-line require-await
   async invoke (_args: DataProviderArgs): Promise<any> {

+ 1 - 0
services/data/hooks/hookDeleter/hookDeleterExample.ts

@@ -8,6 +8,7 @@ class HookDeleterExample extends BaseHook implements HookDeleter {
   async invoke (args: DataDeleterArgs): Promise<any> {
     // eslint-disable-next-line no-console
     await console.log('This is a deleter hook')
+    // don't forget to include this class ins the _import.ts file
   }
 
   static support (_args: DataDeleterArgs): boolean {

+ 3 - 1
services/data/hooks/hookPersister/_import.ts

@@ -1,5 +1,7 @@
 import HookPersisterExample from '~/services/data/hooks/hookPersister/hookPersisterExample'
+import PostPersistProfileRefresh from "~/services/data/hooks/hookPersister/postPersistProfileRefresh";
 
 export const hooksPersister = [
-  HookPersisterExample
+  HookPersisterExample,
+  PostPersistProfileRefresh
 ]

+ 1 - 0
services/data/hooks/hookPersister/hookPersisterExample.ts

@@ -7,6 +7,7 @@ class HookPersisterExample extends BaseHook implements HookPersister {
   async invoke (_args: DataPersisterArgs): Promise<any> {
     // eslint-disable-next-line no-console
     await console.log('This is a persister hook')
+    // don't forget to include this class ins the _import.ts file
   }
 
   static support (_args: DataPersisterArgs): boolean {

+ 22 - 0
services/data/hooks/hookPersister/postPersistProfileRefresh.ts

@@ -0,0 +1,22 @@
+import { DataPersisterArgs, HookPersister } from '~/types/interfaces'
+import BaseHook from '~/services/data/hooks/baseHook'
+import DataPersister from "~/services/data/dataPersister";
+
+/**
+ * Access profile (and its embed organization profile) shall be re-fetched after some
+ * data updates
+ */
+class PostPersistProfileRefresh extends BaseHook implements HookPersister {
+  public static  priority = 10
+
+  async invoke (_args: DataPersisterArgs): Promise<any> {
+    await (this.parent as DataPersister).fetchProfile()
+  }
+
+  static support (_args: DataPersisterArgs): boolean {
+    return (typeof _args.model !== 'undefined') &&
+      ['accesses', 'organizations', 'parameters', 'subdomains'].includes(_args.model.entity)
+  }
+}
+
+export default PostPersistProfileRefresh

+ 1 - 0
services/data/hooks/hookProvider/hookProviderExample.ts

@@ -7,6 +7,7 @@ class HookProviderExample extends BaseHook implements HookProvider {
   async invoke (_args: DataProviderArgs): Promise<any> {
     // eslint-disable-next-line no-console
     await console.log('This is a provider hook')
+    // don't forget to include this class ins the _import.ts file
   }
 
   static support (_args: DataProviderArgs): boolean {

+ 5 - 0
services/profile/organizationProfile.ts

@@ -155,6 +155,10 @@ class OrganizationProfile {
     return false
   }
 
+  getWebsite(): string | null {
+    return this.organizationProfile.website ?? null
+  }
+
   /**
    * Factory
    *
@@ -170,6 +174,7 @@ class OrganizationProfile {
       isAssociation: this.isAssociation.bind(this),
       isShowAdherentList: this.isShowAdherentList.bind(this),
       isCmf: this.isCmf.bind(this),
+      getWebsite: this.getWebsite.bind(this),
     }
   }
 }

+ 14 - 0
store/index.js

@@ -51,5 +51,19 @@ export const actions = {
       url: '/api/my_profile'
     })
     await dispatch('profile/access/setProfile', myProfile.data)
+  },
+
+  /**
+   * Met à jour les informations du profil connecté, par exemple après une mise à jour des paramètres
+   * @param dispatch
+   * @param state
+   * @return {Promise<void>}
+   */
+  async refreshUserProfile ({ dispatch }) {
+    const myProfile = await this.app.context.$dataProvider.invoke({
+      type: QUERY_TYPE.DEFAULT,
+      url: '/api/my_profile'
+    })
+    await dispatch('profile/access/refreshProfile', myProfile.data)
   }
 }

+ 9 - 0
store/profile/access.ts

@@ -174,6 +174,15 @@ export const actions = {
 
     context.dispatch('createNewMyProfileVUexOrmInstance', profile)
   },
+  refreshProfile (context: any, profile: any) {
+    context.commit('setName', profile.name)
+    context.commit('setGivenName', profile.givenName)
+    context.commit('setGender', profile.gender)
+    context.commit('setAvatarId', profile.avatarId)
+    context.commit('setActivityYear', profile.activityYear)
+
+    context.dispatch('profile/organization/refreshProfile', profile.organization, { root: true })
+  },
   setMultiAccesses (context: any, organizations: any) {
     _.each(organizations, (organization:baseOrganizationState) => {
       const o: baseOrganizationState = {

+ 7 - 7
store/profile/organization.ts

@@ -13,7 +13,6 @@ export const state = () => ({
   showAdherentList: false,
   networks: [],
   website: '',
-  subDomain: '',
   parents: []
 })
 
@@ -54,9 +53,6 @@ export const mutations = {
   setWebsite (state: organizationState, website: string) {
     state.website = website
   },
-  setSubDomain (state: organizationState, subDomain: string) {
-    state.subDomain = subDomain
-  },
   addParent (state: organizationState, parent: organizationState) {
     state.parents.push(parent)
   }
@@ -70,7 +66,6 @@ export const actions = {
     context.commit('setProduct', profile.product)
     context.commit('setCurrentActivityYear', profile.currentYear)
     context.commit('setWebsite', profile.website)
-    context.commit('setSubDomain', profile.subDomain)
     context.commit('setModules', profile.modules)
     context.commit('setHasChildren', profile.hasChildren)
     context.commit('setLegalStatus', profile.legalStatus)
@@ -81,10 +76,15 @@ export const actions = {
       const p: baseOrganizationState = {
         id: parent.id,
         name: parent.name,
-        website: parent.website,
-        subDomain: parent.subDomain
+        website: parent.website
       }
       context.commit('addParent', p)
     })
+  },
+  refreshProfile (context: any, profile: any) {
+    context.commit('setName', profile.name)
+    context.commit('setCurrentActivityYear', profile.currentYear)
+    context.commit('setWebsite', profile.website)
+    context.commit('setLegalStatus', profile.legalStatus)
   }
 }

+ 1 - 2
types/interfaces.d.ts

@@ -135,8 +135,7 @@ interface AccessStore extends Store<{profile:{access: accessState}}> {}
 interface baseOrganizationState {
   id: number,
   name: string,
-  website?: string,
-  subDomain?: string
+  website?: string
 }
 
 interface organizationState extends baseOrganizationState {