]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(ui): global code quality pass
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 09:56:03 +0000 (11:56 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 17 Apr 2026 09:56:03 +0000 (11:56 +0200)
- Fix ToggleButton.vue: replace unsafe for...in on localStorage with
  Object.keys(), cache localStorage value (4 reads reduced to 1)
- Extract 42-line inline @change handler in ChargingStationsView.vue
  to handleUIServerChange method
- Move ServerFailureError from WebSocketClient.ts to errors.ts
  (proper separation of concerns, re-exported for backward compat)
- Simplify useExecuteAction callback param: remove union type,
  use only ExecuteActionCallbacks object; update call sites
- Remove unnecessary String() in cli table.ts template literals

ui/cli/src/output/table.ts
ui/common/src/client/WebSocketClient.ts
ui/common/src/errors.ts
ui/web/src/components/actions/AddChargingStations.vue
ui/web/src/components/actions/SetSupervisionUrl.vue
ui/web/src/components/buttons/ToggleButton.vue
ui/web/src/composables/Utils.ts
ui/web/src/views/ChargingStationsView.vue
ui/web/tests/unit/ChargingStationsView.test.ts

index ec971d7e7ec32dc0d345ee35bf9bfceb02853552..e78ac24b166de56177c4506755cd413a2bda7a17 100644 (file)
@@ -13,13 +13,15 @@ const hashIdTable = (ids: string[]) => {
 
 export const outputTable = (payload: ResponsePayload): void => {
   if (payload.hashIdsSucceeded != null && payload.hashIdsSucceeded.length > 0) {
-    process.stdout.write(chalk.green(`✓ Succeeded (${String(payload.hashIdsSucceeded.length)}):\n`))
+    process.stdout.write(
+      chalk.green(`✓ Succeeded (${payload.hashIdsSucceeded.length.toString()}):\n`)
+    )
     const table = hashIdTable(payload.hashIdsSucceeded)
     process.stdout.write(table.toString() + '\n')
   }
 
   if (payload.hashIdsFailed != null && payload.hashIdsFailed.length > 0) {
-    process.stderr.write(chalk.red(`✗ Failed (${String(payload.hashIdsFailed.length)}):\n`))
+    process.stderr.write(chalk.red(`✗ Failed (${payload.hashIdsFailed.length.toString()}):\n`))
     if (payload.responsesFailed != null && payload.responsesFailed.length > 0) {
       const table = new Table({ head: [chalk.white('Hash ID'), chalk.white('Error')] })
       for (const entry of payload.responsesFailed) {
index 273d568b3f06cd8aaf7a10265cba45b0a69909c4..531238edf1a0a3a13e78a5c3335c742d74c4e540 100644 (file)
@@ -3,23 +3,12 @@ import type { UUIDv4 } from '../types/UUID.js'
 import type { ClientConfig, ResponseHandler, WebSocketFactory, WebSocketLike } from './types.js'
 
 import { UI_WEBSOCKET_REQUEST_TIMEOUT_MS } from '../constants.js'
+import { ServerFailureError } from '../errors.js'
 import { AuthenticationType, ResponseStatus } from '../types/UIProtocol.js'
 import { randomUUID, validateUUID } from '../utils/UUID.js'
 import { WebSocketReadyState } from './types.js'
 
-export class ServerFailureError extends Error {
-  public readonly payload: ResponsePayload
-
-  public constructor (payload: ResponsePayload) {
-    const details =
-      payload.hashIdsFailed != null && payload.hashIdsFailed.length > 0
-        ? `: ${payload.hashIdsFailed.length.toString()} station(s) failed`
-        : ''
-    super(`Server returned failure status${details}`)
-    this.name = 'ServerFailureError'
-    this.payload = payload
-  }
-}
+export { ServerFailureError } from '../errors.js'
 
 export class WebSocketClient {
   public get url (): string {
index 64a9e2618a174f02b12236a9fdb48ab7eb7b1ab2..6aebfe90199eb31f64424757321bddf8a6b246e3 100644 (file)
@@ -1,3 +1,5 @@
+import type { ResponsePayload } from './types/UIProtocol.js'
+
 export class ConnectionError extends Error {
   public readonly url: string
 
@@ -12,5 +14,19 @@ export class ConnectionError extends Error {
   }
 }
 
+export class ServerFailureError extends Error {
+  public readonly payload: ResponsePayload
+
+  public constructor (payload: ResponsePayload) {
+    const details =
+      payload.hashIdsFailed != null && payload.hashIdsFailed.length > 0
+        ? `: ${payload.hashIdsFailed.length.toString()} station(s) failed`
+        : ''
+    super(`Server returned failure status${details}`)
+    this.name = 'ServerFailureError'
+    this.payload = payload
+  }
+}
+
 export const extractErrorMessage = (error: unknown): string =>
   error instanceof Error ? error.message : String(error)
index b133ac98c7fc780d97c844172dde3a62dac012cb..1dafe5670788980f5de50a69de5d33beee2b0549 100644 (file)
@@ -145,9 +145,11 @@ const addChargingStations = (): void => {
     }),
     'Charging stations successfully added',
     'Error at adding charging stations',
-    () => {
-      resetToggleButtonState('add-charging-stations', true)
-      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    {
+      onFinally: () => {
+        resetToggleButtonState('add-charging-stations', true)
+        $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+      },
     }
   )
 }
index bcde5ff1ed5d171fcbb181df633db8347d187e35..133e3fc6c64646d7e9e924e615af161d905c8dfd 100644 (file)
@@ -46,9 +46,11 @@ const setSupervisionUrl = (): void => {
     $uiClient.setSupervisionUrl(props.hashId, state.value.supervisionUrl),
     'Supervision url successfully set',
     'Error at setting supervision url',
-    () => {
-      resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
-      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+    {
+      onFinally: () => {
+        resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
+        $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+      },
     }
   )
 }
index dc6ff7fc96e1241927721285cb483f61216f1e61..b42146dddf2e873e7f1672ce2e4ec45dc71b5bb0 100644 (file)
@@ -39,20 +39,21 @@ const state = ref<{ status: boolean }>({
 
 const click = (): void => {
   if (props.shared === true) {
-    for (const key in localStorage) {
+    for (const key of Object.keys(localStorage)) {
       if (key !== id && key.startsWith(SHARED_TOGGLE_BUTTON_KEY_PREFIX)) {
         setToLocalStorage<boolean>(key, false)
-        state.value.status = getFromLocalStorage<boolean>(key, false)
       }
     }
   }
-  setToLocalStorage<boolean>(id, !getFromLocalStorage<boolean>(id, props.status ?? false))
-  state.value.status = getFromLocalStorage<boolean>(id, props.status ?? false)
-  if (getFromLocalStorage<boolean>(id, props.status ?? false)) {
+  const current = getFromLocalStorage<boolean>(id, props.status ?? false)
+  const newStatus = !current
+  setToLocalStorage<boolean>(id, newStatus)
+  state.value.status = newStatus
+  if (newStatus) {
     props.on?.()
   } else {
     props.off?.()
   }
-  $emit('clicked', getFromLocalStorage<boolean>(id, props.status ?? false))
+  $emit('clicked', newStatus)
 }
 </script>
index 5faae924fa880b6d7fba0db1d494295fc29faada..17e0a649e1586a31e669bff2b3f70af23d7e05c3 100644 (file)
@@ -94,12 +94,9 @@ export const useExecuteAction = (emit?: (event: 'need-refresh') => void) => {
     action: Promise<unknown>,
     successMsg: string,
     errorMsg: string,
-    callbacks?: (() => void) | ExecuteActionCallbacks
+    callbacks?: ExecuteActionCallbacks
   ): void => {
-    const { onFinally, onSuccess } =
-      typeof callbacks === 'function'
-        ? { onFinally: callbacks, onSuccess: undefined }
-        : (callbacks ?? {})
+    const { onFinally, onSuccess } = callbacks ?? {}
     action
       .then(() => {
         try {
index 50aa28e427b70e76e2e960b5b2ffc42585754bf9..fad7a437923212941eec8fed8c36629229c38ba6 100644 (file)
           id="ui-server-selector"
           v-model="state.uiServerIndex"
           class="ui-server-selector"
-          @change="
-            () => {
-              if (
-                getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0) !==
-                state.uiServerIndex
-              ) {
-                $uiClient.setConfiguration(
-                  ($configuration.uiServer as UIServerConfigurationSection[])[state.uiServerIndex]
-                )
-                registerWSEventListeners()
-                $uiClient.registerWSEventListener(
-                  'open',
-                  () => {
-                    setToLocalStorage<number>(
-                      UI_SERVER_CONFIGURATION_INDEX_KEY,
-                      state.uiServerIndex
-                    )
-                    clearToggleButtons()
-                    refresh()
-                    $route.name !== ROUTE_NAMES.CHARGING_STATIONS &&
-                      $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
-                  },
-                  { once: true }
-                )
-                $uiClient.registerWSEventListener(
-                  'error',
-                  () => {
-                    state.uiServerIndex = getFromLocalStorage<number>(
-                      UI_SERVER_CONFIGURATION_INDEX_KEY,
-                      0
-                    )
-                    $uiClient.setConfiguration(
-                      ($configuration.uiServer as UIServerConfigurationSection[])[
-                        getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
-                      ]
-                    )
-                    registerWSEventListeners()
-                  },
-                  { once: true }
-                )
-              }
-            }
-          "
+          @change="handleUIServerChange"
         >
           <option
             v-for="uiServerConfiguration in uiServerConfigurations"
@@ -112,6 +70,7 @@ import {
   type UUIDv4,
 } from 'ui-common'
 import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 
 import StateButton from '@/components/buttons/StateButton.vue'
 import ToggleButton from '@/components/buttons/ToggleButton.vue'
@@ -163,6 +122,8 @@ const clearToggleButtons = (): void => {
 const $configuration = useConfiguration()
 const $templates = useTemplates()
 const $chargingStations = useChargingStations()
+const $route = useRoute()
+const $router = useRouter()
 
 watch($chargingStations, () => {
   state.value.renderChargingStations = randomUUID()
@@ -224,6 +185,46 @@ const unregisterWSEventListeners = () => {
   $uiClient.unregisterWSEventListener('close', clearChargingStations)
 }
 
+const handleUIServerChange = (): void => {
+  const currentIndex = getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0)
+  if (currentIndex === state.value.uiServerIndex) return
+
+  $uiClient.setConfiguration(
+    ($configuration.value.uiServer as UIServerConfigurationSection[])[state.value.uiServerIndex]
+  )
+  registerWSEventListeners()
+
+  $uiClient.registerWSEventListener(
+    'open',
+    () => {
+      setToLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, state.value.uiServerIndex)
+      clearToggleButtons()
+      refresh()
+      if ($route.name !== ROUTE_NAMES.CHARGING_STATIONS) {
+        $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+      }
+    },
+    { once: true }
+  )
+
+  $uiClient.registerWSEventListener(
+    'error',
+    () => {
+      state.value.uiServerIndex = getFromLocalStorage<number>(
+        UI_SERVER_CONFIGURATION_INDEX_KEY,
+        0
+      )
+      $uiClient.setConfiguration(
+        ($configuration.value.uiServer as UIServerConfigurationSection[])[
+          state.value.uiServerIndex
+        ]
+      )
+      registerWSEventListeners()
+    },
+    { once: true }
+  )
+}
+
 let unsubscribeRefresh: (() => void) | undefined
 
 onMounted(() => {
index a861eb86fe07617e43ec0e52593a2a7cf73f6a89..b4370ede7c09e1afc11c86b2c37e532f70f67840 100644 (file)
@@ -28,6 +28,15 @@ vi.mock('@/composables', async importOriginal => {
   return { ...(actual as Record<string, unknown>), useUIClient: vi.fn() }
 })
 
+vi.mock('vue-router', async importOriginal => {
+  const actual: Record<string, unknown> = await importOriginal()
+  return {
+    ...actual,
+    useRoute: vi.fn().mockReturnValue({ name: 'charging-stations' }),
+    useRouter: vi.fn().mockReturnValue({ push: vi.fn() }),
+  }
+})
+
 // ── Configuration fixtures ────────────────────────────────────────────────────
 
 const singleServerConfiguration = {