From 01b9a6aed766a8421c17c8a23ca37646810414de Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 17 Apr 2026 11:56:03 +0200 Subject: [PATCH] refactor(ui): global code quality pass - 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 | 6 +- ui/common/src/client/WebSocketClient.ts | 15 +--- ui/common/src/errors.ts | 16 ++++ .../actions/AddChargingStations.vue | 8 +- .../components/actions/SetSupervisionUrl.vue | 8 +- .../src/components/buttons/ToggleButton.vue | 13 +-- ui/web/src/composables/Utils.ts | 7 +- ui/web/src/views/ChargingStationsView.vue | 87 ++++++++++--------- .../tests/unit/ChargingStationsView.test.ts | 9 ++ 9 files changed, 94 insertions(+), 75 deletions(-) diff --git a/ui/cli/src/output/table.ts b/ui/cli/src/output/table.ts index ec971d7e..e78ac24b 100644 --- a/ui/cli/src/output/table.ts +++ b/ui/cli/src/output/table.ts @@ -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) { diff --git a/ui/common/src/client/WebSocketClient.ts b/ui/common/src/client/WebSocketClient.ts index 273d568b..531238ed 100644 --- a/ui/common/src/client/WebSocketClient.ts +++ b/ui/common/src/client/WebSocketClient.ts @@ -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 { diff --git a/ui/common/src/errors.ts b/ui/common/src/errors.ts index 64a9e261..6aebfe90 100644 --- a/ui/common/src/errors.ts +++ b/ui/common/src/errors.ts @@ -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) diff --git a/ui/web/src/components/actions/AddChargingStations.vue b/ui/web/src/components/actions/AddChargingStations.vue index b133ac98..1dafe567 100644 --- a/ui/web/src/components/actions/AddChargingStations.vue +++ b/ui/web/src/components/actions/AddChargingStations.vue @@ -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 }) + }, } ) } diff --git a/ui/web/src/components/actions/SetSupervisionUrl.vue b/ui/web/src/components/actions/SetSupervisionUrl.vue index bcde5ff1..133e3fc6 100644 --- a/ui/web/src/components/actions/SetSupervisionUrl.vue +++ b/ui/web/src/components/actions/SetSupervisionUrl.vue @@ -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 }) + }, } ) } diff --git a/ui/web/src/components/buttons/ToggleButton.vue b/ui/web/src/components/buttons/ToggleButton.vue index dc6ff7fc..b42146dd 100644 --- a/ui/web/src/components/buttons/ToggleButton.vue +++ b/ui/web/src/components/buttons/ToggleButton.vue @@ -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(key, false) - state.value.status = getFromLocalStorage(key, false) } } } - setToLocalStorage(id, !getFromLocalStorage(id, props.status ?? false)) - state.value.status = getFromLocalStorage(id, props.status ?? false) - if (getFromLocalStorage(id, props.status ?? false)) { + const current = getFromLocalStorage(id, props.status ?? false) + const newStatus = !current + setToLocalStorage(id, newStatus) + state.value.status = newStatus + if (newStatus) { props.on?.() } else { props.off?.() } - $emit('clicked', getFromLocalStorage(id, props.status ?? false)) + $emit('clicked', newStatus) } diff --git a/ui/web/src/composables/Utils.ts b/ui/web/src/composables/Utils.ts index 5faae924..17e0a649 100644 --- a/ui/web/src/composables/Utils.ts +++ b/ui/web/src/composables/Utils.ts @@ -94,12 +94,9 @@ export const useExecuteAction = (emit?: (event: 'need-refresh') => void) => { action: Promise, 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 { diff --git a/ui/web/src/views/ChargingStationsView.vue b/ui/web/src/views/ChargingStationsView.vue index 50aa28e4..fad7a437 100644 --- a/ui/web/src/views/ChargingStationsView.vue +++ b/ui/web/src/views/ChargingStationsView.vue @@ -10,49 +10,7 @@ id="ui-server-selector" v-model="state.uiServerIndex" class="ui-server-selector" - @change=" - () => { - if ( - getFromLocalStorage(UI_SERVER_CONFIGURATION_INDEX_KEY, 0) !== - state.uiServerIndex - ) { - $uiClient.setConfiguration( - ($configuration.uiServer as UIServerConfigurationSection[])[state.uiServerIndex] - ) - registerWSEventListeners() - $uiClient.registerWSEventListener( - 'open', - () => { - setToLocalStorage( - 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( - UI_SERVER_CONFIGURATION_INDEX_KEY, - 0 - ) - $uiClient.setConfiguration( - ($configuration.uiServer as UIServerConfigurationSection[])[ - getFromLocalStorage(UI_SERVER_CONFIGURATION_INDEX_KEY, 0) - ] - ) - registerWSEventListeners() - }, - { once: true } - ) - } - } - " + @change="handleUIServerChange" >