]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(webui): migrate globalProperties to provide/inject
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 3 Apr 2026 11:57:37 +0000 (13:57 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 3 Apr 2026 11:57:37 +0000 (13:57 +0200)
Replace Vue 2-style app.config.globalProperties with Vue 3
provide/inject using typed InjectionKeys. Create useConfiguration
and useTemplates composables. Remove dead globalProperties
fallback paths and refreshChargingStations no-op. Update test
mounts to use provide. Clean ComponentCustomProperties from
shims-vue.d.ts.

ui/web/src/components/actions/AddChargingStations.vue
ui/web/src/components/actions/SetSupervisionUrl.vue
ui/web/src/components/actions/StartTransaction.vue
ui/web/src/composables/Utils.ts
ui/web/src/composables/index.ts
ui/web/src/main.ts
ui/web/src/shims-vue.d.ts
ui/web/src/views/ChargingStationsView.vue
ui/web/tests/unit/AddChargingStations.test.ts
ui/web/tests/unit/ChargingStationsView.test.ts
ui/web/tests/unit/SetSupervisionUrl.test.ts

index 83de85296754bcfa2265655f88a55eda29a4185f..d756ef3cd4686b47214d5783ef55361d2645c092 100644 (file)
@@ -14,8 +14,8 @@
       Please select a template
     </option>
     <option
-      v-for="template in $templates!.value"
-      v-show="Array.isArray($templates?.value) && $templates.value.length > 0"
+      v-for="template in $templates"
+      v-show="Array.isArray($templates) && $templates.length > 0"
       :key="template"
     >
       {{ template }}
@@ -87,7 +87,7 @@
     @click="
       () => {
         $uiClient
-          ?.addChargingStations(state.template, state.numberOfStations, {
+          .addChargingStations(state.template, state.numberOfStations, {
             supervisionUrls: state.supervisionUrl.length > 0 ? state.supervisionUrl : undefined,
             autoStart: convertToBoolean(state.autoStart),
             persistentConfiguration: convertToBoolean(state.persistentConfiguration),
           })
           .then(() => {
             $toast.success('Charging stations successfully added')
-            return refreshChargingStations()
-          })
-          .catch((error: Error) => {
-            $toast.error('Error at adding charging stations')
-            console.error('Error at adding charging stations:', error)
           })
           .finally(() => {
             resetToggleButtonState('add-charging-stations', true)
             $router.push({ name: 'charging-stations' })
           })
+          .catch((error: Error) => {
+            $toast.error('Error at adding charging stations')
+            console.error('Error at adding charging stations:', error)
+          })
       }
     "
   >
 </template>
 
 <script setup lang="ts">
-import { getCurrentInstance, ref, watch } from 'vue'
+import { ref, watch } from 'vue'
 
 import type { UUIDv4 } from '@/types'
 
@@ -122,8 +121,9 @@ import Button from '@/components/buttons/Button.vue'
 import {
   convertToBoolean,
   randomUUID,
-  refreshChargingStations,
   resetToggleButtonState,
+  useTemplates,
+  useUIClient,
 } from '@/composables'
 
 const state = ref<{
@@ -146,14 +146,12 @@ const state = ref<{
   template: '',
 })
 
-const app = getCurrentInstance()
+const $uiClient = useUIClient()
+const $templates = useTemplates()
 
-const templates = app?.appContext.config.globalProperties.$templates
-if (templates != null) {
-  watch(templates, () => {
-    state.value.renderTemplates = randomUUID()
-  })
-}
+watch($templates, () => {
+  state.value.renderTemplates = randomUUID()
+})
 </script>
 
 <style scoped>
index 6406467f953e9d2d451cd4577fdf2544e3ce6199..045ccc74f0e684b95276c5fbd26f4dbdbccb03f5 100644 (file)
     @click="
       () => {
         $uiClient
-          ?.setSupervisionUrl(hashId, state.supervisionUrl)
+          .setSupervisionUrl(hashId, state.supervisionUrl)
           .then(() => {
             $toast.success('Supervision url successfully set')
-            return refreshChargingStations()
-          })
-          .catch((error: Error) => {
-            $toast.error('Error at setting supervision url')
-            console.error('Error at setting supervision url:', error)
           })
           .finally(() => {
             resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
             $router.push({ name: 'charging-stations' })
           })
+          .catch((error: Error) => {
+            $toast.error('Error at setting supervision url')
+            console.error('Error at setting supervision url:', error)
+          })
       }
     "
   >
@@ -42,7 +41,7 @@
 import { ref } from 'vue'
 
 import Button from '@/components/buttons/Button.vue'
-import { refreshChargingStations, resetToggleButtonState } from '@/composables'
+import { resetToggleButtonState, useUIClient } from '@/composables'
 
 const props = defineProps<{
   chargingStationId: string
@@ -52,6 +51,8 @@ const props = defineProps<{
 const state = ref<{ supervisionUrl: string }>({
   supervisionUrl: '',
 })
+
+const $uiClient = useUIClient()
 </script>
 
 <style scoped>
index abfe0fe48dc0bbc3767f982c95ba25e91d875925..9365d72deaaba37e62e45da3b6d29f0949f9b7ee 100644 (file)
@@ -42,13 +42,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { useToast } from 'vue-toast-notification'
 
 import Button from '@/components/buttons/Button.vue'
-import {
-  convertToInt,
-  refreshChargingStations,
-  resetToggleButtonState,
-  UIClient,
-  useUIClient,
-} from '@/composables'
+import { convertToInt, resetToggleButtonState, UIClient, useUIClient } from '@/composables'
 import { type OCPPVersion } from '@/types'
 
 const props = defineProps<{
@@ -105,7 +99,6 @@ const handleStartTransaction = async (): Promise<void> => {
       ocppVersion: ocppVersion.value,
     })
     $toast.success('Transaction successfully started')
-    await refreshChargingStations()
   } catch (error) {
     $toast.error('Error at starting transaction')
     console.error('Error at starting transaction:', error)
index c5531deda351dc167ac514d084839545313121b9..fbc719802bc50bb8e5188f4aa7c74f88786f2baf 100644 (file)
@@ -1,12 +1,18 @@
-import type { Ref } from 'vue'
+import type { InjectionKey, Ref } from 'vue'
 
-import { getCurrentInstance } from 'vue'
+import { inject } from 'vue'
 import { useToast } from 'vue-toast-notification'
 
-import type { ChargingStationData, UUIDv4 } from '@/types'
+import type { ChargingStationData, ConfigurationData, UUIDv4 } from '@/types'
 
 import { UIClient } from './UIClient'
 
+export const configurationKey: InjectionKey<Ref<ConfigurationData>> = Symbol('configuration')
+export const chargingStationsKey: InjectionKey<Ref<ChargingStationData[]>> =
+  Symbol('chargingStations')
+export const templatesKey: InjectionKey<Ref<string[]>> = Symbol('templates')
+export const uiClientKey: InjectionKey<UIClient> = Symbol('uiClient')
+
 export const convertToBoolean = (value: unknown): boolean => {
   let result = false
   if (value != null) {
@@ -86,18 +92,27 @@ export const validateUUID = (uuid: unknown): uuid is UUIDv4 => {
 }
 
 export const useUIClient = (): UIClient => {
+  const injected = inject(uiClientKey, undefined)
+  if (injected != null) return injected
   return UIClient.getInstance()
 }
 
-export const useChargingStations = (): Ref<ChargingStationData[]> | undefined => {
-  return getCurrentInstance()?.appContext.config.globalProperties.$chargingStations
+export const useConfiguration = (): Ref<ConfigurationData> => {
+  const injected = inject(configurationKey, undefined)
+  if (injected != null) return injected
+  throw new Error('configuration not provided')
+}
+
+export const useChargingStations = (): Ref<ChargingStationData[]> => {
+  const injected = inject(chargingStationsKey, undefined)
+  if (injected != null) return injected
+  throw new Error('chargingStations not provided')
 }
 
-export const refreshChargingStations = async (): Promise<void> => {
-  const $chargingStations = useChargingStations()
-  if ($chargingStations == null) return
-  const response = await useUIClient().listChargingStations()
-  $chargingStations.value = response.chargingStations as ChargingStationData[]
+export const useTemplates = (): Ref<string[]> => {
+  const injected = inject(templatesKey, undefined)
+  if (injected != null) return injected
+  throw new Error('templates not provided')
 }
 
 export const useExecuteAction = (emit: (event: 'need-refresh') => void) => {
index 8b8fb6ef7db30010c04c212f1e8610ada02ba71d..d7cf7eff18cb52c690ed19adf43324783567ecc8 100644 (file)
@@ -1,15 +1,20 @@
 export { UIClient } from './UIClient'
 export {
+  chargingStationsKey,
+  configurationKey,
   convertToBoolean,
   convertToInt,
   deleteFromLocalStorage,
   getFromLocalStorage,
   getLocalStorage,
   randomUUID,
-  refreshChargingStations,
   resetToggleButtonState,
   setToLocalStorage,
+  templatesKey,
+  uiClientKey,
   useChargingStations,
+  useConfiguration,
   useExecuteAction,
+  useTemplates,
   useUIClient,
 } from './Utils'
index ea0c198fba35613f34a5b59da634e2a28a446ca0..d9fcfc9b1a71d85d2b948100e896e440ecea1203 100644 (file)
@@ -4,7 +4,15 @@ import ToastPlugin from 'vue-toast-notification'
 import type { ChargingStationData, ConfigurationData, UIServerConfigurationSection } from '@/types'
 
 import App from '@/App.vue'
-import { getFromLocalStorage, setToLocalStorage, UIClient } from '@/composables'
+import {
+  chargingStationsKey,
+  configurationKey,
+  getFromLocalStorage,
+  setToLocalStorage,
+  templatesKey,
+  UIClient,
+  uiClientKey,
+} from '@/composables'
 import { router } from '@/router'
 
 import 'vue-toast-notification/dist/theme-bootstrap.css'
@@ -33,27 +41,25 @@ const initializeApp = async (app: AppType, config: ConfigurationData): Promise<v
   if (!Array.isArray(config.uiServer)) {
     config.uiServer = [config.uiServer]
   }
-  app.config.globalProperties.$configuration ??= ref(config)
-  if (!Array.isArray(app.config.globalProperties.$templates?.value)) {
-    app.config.globalProperties.$templates = ref<string[]>([])
-  }
-  if (!Array.isArray(app.config.globalProperties.$chargingStations?.value)) {
-    app.config.globalProperties.$chargingStations = ref<ChargingStationData[]>([])
-  }
+  const configuration = ref(config)
+  const templates = ref<string[]>([])
+  const chargingStations = ref<ChargingStationData[]>([])
   if (
     getFromLocalStorage<number | undefined>('uiServerConfigurationIndex', undefined) == null ||
     getFromLocalStorage('uiServerConfigurationIndex', 0) >
-      (app.config.globalProperties.$configuration.value.uiServer as UIServerConfigurationSection[])
-        .length -
-        1
+      (configuration.value.uiServer as UIServerConfigurationSection[]).length - 1
   ) {
     setToLocalStorage('uiServerConfigurationIndex', 0)
   }
-  app.config.globalProperties.$uiClient ??= UIClient.getInstance(
-    (app.config.globalProperties.$configuration.value.uiServer as UIServerConfigurationSection[])[
+  const uiClient = UIClient.getInstance(
+    (configuration.value.uiServer as UIServerConfigurationSection[])[
       getFromLocalStorage('uiServerConfigurationIndex', 0)
     ]
   )
+  app.provide(configurationKey, configuration)
+  app.provide(chargingStationsKey, chargingStations)
+  app.provide(templatesKey, templates)
+  app.provide(uiClientKey, uiClient)
   app.use(router).use(ToastPlugin).mount('#app')
 }
 
index 13685ebb1076c6fcdd0f9e996214265d624b3b60..10ff1ef1513a6aca8be0a2070dfcaa7f78ed34b7 100644 (file)
@@ -5,10 +5,4 @@ declare module 'vue' {
     RouterLink: (typeof import('vue-router'))['RouterLink']
     RouterView: (typeof import('vue-router'))['RouterView']
   }
-  interface ComponentCustomProperties {
-    $chargingStations: import('vue').Ref<import('@/types').ChargingStationData[]> | undefined
-    $configuration: import('vue').Ref<import('@/types').ConfigurationData> | undefined
-    $templates: import('vue').Ref<string[]> | undefined
-    $uiClient: import('@/composables').UIClient | undefined
-  }
 }
index 386525430a4d2da52265e4f4b52c955c3085c4c5..a50a67937d82bcd969b1c49eea02f2eaff8abe4b 100644 (file)
               if (
                 getFromLocalStorage<number>('uiServerConfigurationIndex', 0) !== state.uiServerIndex
               ) {
-                $uiClient?.setConfiguration(
-                  ($configuration!.value.uiServer as UIServerConfigurationSection[])[
-                    state.uiServerIndex
-                  ]
+                $uiClient.setConfiguration(
+                  ($configuration.uiServer as UIServerConfigurationSection[])[state.uiServerIndex]
                 )
                 registerWSEventListeners()
-                $uiClient?.registerWSEventListener(
+                $uiClient.registerWSEventListener(
                   'open',
                   () => {
                     setToLocalStorage<number>('uiServerConfigurationIndex', state.uiServerIndex)
                   },
                   { once: true }
                 )
-                $uiClient?.registerWSEventListener(
+                $uiClient.registerWSEventListener(
                   'error',
                   () => {
                     state.uiServerIndex = getFromLocalStorage<number>(
                       'uiServerConfigurationIndex',
                       0
                     )
-                    $uiClient?.setConfiguration(
-                      ($configuration!.value.uiServer as UIServerConfigurationSection[])[
+                    $uiClient.setConfiguration(
+                      ($configuration.uiServer as UIServerConfigurationSection[])[
                         getFromLocalStorage<number>('uiServerConfigurationIndex', 0)
                       ]
                     )
@@ -99,9 +97,9 @@
       />
     </Container>
     <CSTable
-      v-show="Array.isArray($chargingStations?.value) && $chargingStations.value.length > 0"
+      v-show="Array.isArray($chargingStations) && $chargingStations.length > 0"
       :key="state.renderChargingStations"
-      :charging-stations="$chargingStations!.value"
+      :charging-stations="$chargingStations"
       @need-refresh="
         () => {
           getChargingStations()
 </template>
 
 <script setup lang="ts">
-import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
 import { useToast } from 'vue-toast-notification'
 
 import type {
@@ -137,6 +135,8 @@ import {
   randomUUID,
   setToLocalStorage,
   useChargingStations,
+  useConfiguration,
+  useTemplates,
   useUIClient,
 } from '@/composables'
 
@@ -178,25 +178,20 @@ const clearToggleButtons = (): void => {
   }
 }
 
-const app = getCurrentInstance()
+const $configuration = useConfiguration()
+const $templates = useTemplates()
+const $chargingStations = useChargingStations()
 
-const chargingStationsRef = useChargingStations()
-if (chargingStationsRef != null) {
-  watch(chargingStationsRef, () => {
-    state.value.renderChargingStations = randomUUID()
-  })
-}
+watch($chargingStations, () => {
+  state.value.renderChargingStations = randomUUID()
+})
 
 const clearTemplates = (): void => {
-  if (app != null) {
-    app.appContext.config.globalProperties.$templates!.value = []
-  }
+  $templates.value = []
 }
 
 const clearChargingStations = (): void => {
-  if (chargingStationsRef != null) {
-    chargingStationsRef.value = []
-  }
+  $chargingStations.value = []
 }
 
 const $uiClient = useUIClient()
@@ -228,9 +223,7 @@ const getTemplates = (): void => {
     $uiClient
       .listTemplates()
       .then((response: ResponsePayload) => {
-        if (app != null) {
-          app.appContext.config.globalProperties.$templates!.value = response.templates as string[]
-        }
+        $templates.value = response.templates as string[]
         return undefined
       })
       .finally(() => {
@@ -250,9 +243,7 @@ const getChargingStations = (): void => {
     $uiClient
       .listChargingStations()
       .then((response: ResponsePayload) => {
-        if (chargingStationsRef != null) {
-          chargingStationsRef.value = response.chargingStations as ChargingStationData[]
-        }
+        $chargingStations.value = response.chargingStations as ChargingStationData[]
         return undefined
       })
       .finally(() => {
@@ -301,13 +292,12 @@ onUnmounted(() => {
 const uiServerConfigurations: {
   configuration: UIServerConfigurationSection
   index: number
-}[] = (
-  app!.appContext.config.globalProperties.$configuration!.value
-    .uiServer as UIServerConfigurationSection[]
-).map((configuration: UIServerConfigurationSection, index: number) => ({
-  configuration,
-  index,
-}))
+}[] = ($configuration.value.uiServer as UIServerConfigurationSection[]).map(
+  (configuration: UIServerConfigurationSection, index: number) => ({
+    configuration,
+    index,
+  })
+)
 
 const startSimulator = (): void => {
   $uiClient
index 4a7d0d207d61b9841a65c5b2503b8f981c54fe26..4d6b88cb600a2d5e0c76acb001a3e63b6a647c80 100644 (file)
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from 'vitest'
 import { ref } from 'vue'
 
 import AddChargingStations from '@/components/actions/AddChargingStations.vue'
+import { templatesKey, uiClientKey } from '@/composables'
 
 import { toastMock } from '../setup'
 import { ButtonStub, createMockUIClient, type MockUIClient } from './helpers'
@@ -27,11 +28,13 @@ describe('AddChargingStations', () => {
         config: {
           globalProperties: {
             $router: mockRouter,
-            $templates: ref(['template-A.json', 'template-B.json']),
             $toast: toastMock,
-            $uiClient: mockClient,
           } as never,
         },
+        provide: {
+          [templatesKey as symbol]: ref(['template-A.json', 'template-B.json']),
+          [uiClientKey as symbol]: mockClient,
+        },
         stubs: {
           Button: ButtonStub,
         },
index 50e411e89f196bf5fec4ddfce5d9a06a604b6a01..10c573b30b3654413a9c560d0c91798918ac7d4c 100644 (file)
@@ -9,7 +9,13 @@ import { ref } from 'vue'
 
 import type { UIClient } from '@/composables'
 
-import { useUIClient } from '@/composables'
+import {
+  chargingStationsKey,
+  configurationKey,
+  templatesKey,
+  uiClientKey,
+  useUIClient,
+} from '@/composables'
 import { ResponseStatus } from '@/types'
 import ChargingStationsView from '@/views/ChargingStationsView.vue'
 
@@ -77,14 +83,16 @@ function mountView (
     global: {
       config: {
         globalProperties: {
-          $chargingStations: ref(chargingStations),
-          $configuration: ref(configuration),
           $route: { name: 'charging-stations', params: {}, query: {} },
           $router: { back: vi.fn(), push: vi.fn(), replace: vi.fn() },
-          $templates: ref(templates),
-          $uiClient: mockClient,
         } as never,
       },
+      provide: {
+        [chargingStationsKey as symbol]: ref(chargingStations),
+        [configurationKey as symbol]: ref(configuration),
+        [templatesKey as symbol]: ref(templates),
+        [uiClientKey as symbol]: mockClient,
+      },
       stubs: {
         Container: { name: 'Container', template: '<div><slot /></div>' },
         CSTable: true,
index b8bd56a6b9454ddb82429572890a22b7b43b941a..273e08cbfb752c5a1ed0fa4506b380fbc7bdd0a4 100644 (file)
@@ -6,6 +6,7 @@ import { flushPromises, mount } from '@vue/test-utils'
 import { describe, expect, it, vi } from 'vitest'
 
 import SetSupervisionUrl from '@/components/actions/SetSupervisionUrl.vue'
+import { uiClientKey } from '@/composables'
 
 import { toastMock } from '../setup'
 import { TEST_HASH_ID, TEST_STATION_ID } from './constants'
@@ -29,9 +30,11 @@ describe('SetSupervisionUrl', () => {
           globalProperties: {
             $router: mockRouter,
             $toast: toastMock,
-            $uiClient: mockClient,
           } as never,
         },
+        provide: {
+          [uiClientKey as symbol]: mockClient,
+        },
         stubs: {
           Button: ButtonStub,
         },