From 9eab3d0f3c77975e247925c8d822467570b4dc81 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 25 Mar 2026 21:30:03 +0100 Subject: [PATCH] refactor(webui): add StateButton, centralize active style, fix refresh lifecycle - StateButton: stateless toggle driven by server state (active prop) - Button: owns .button--active CSS, ToggleButton/StateButton pass :active - Conditional buttons: show Start/Stop, Open/Close, Lock/Unlock based on state - refreshChargingStations(): shared composable eliminates 4x duplicated refresh - executeAction() helper in CSData/CSConnector: DRY action/toast/refresh - Fix: trigger getChargingStations() on need-refresh event - Fix: refresh after AddChargingStations, StartTransaction, SetSupervisionUrl - table-layout: fixed on connectors table to prevent overflow --- .../actions/AddChargingStations.vue | 12 +- .../components/actions/SetSupervisionUrl.vue | 3 +- .../components/actions/StartTransaction.vue | 9 +- ui/web/src/components/buttons/Button.vue | 20 ++- ui/web/src/components/buttons/StateButton.vue | 20 +++ .../src/components/buttons/ToggleButton.vue | 11 +- .../charging-stations/CSConnector.vue | 119 +++++++++--------- .../components/charging-stations/CSData.vue | 93 +++++++------- ui/web/src/composables/Utils.ts | 17 ++- ui/web/src/composables/index.ts | 2 + ui/web/src/views/ChargingStationsView.vue | 30 ++--- ui/web/tests/unit/CSConnector.test.ts | 12 +- ui/web/tests/unit/CSData.test.ts | 15 +-- .../tests/unit/ChargingStationsView.test.ts | 26 ++-- ui/web/tests/unit/ToggleButton.test.ts | 26 ++-- ui/web/tests/unit/helpers.ts | 8 ++ 16 files changed, 245 insertions(+), 178 deletions(-) create mode 100644 ui/web/src/components/buttons/StateButton.vue diff --git a/ui/web/src/components/actions/AddChargingStations.vue b/ui/web/src/components/actions/AddChargingStations.vue index 0a874599..83de8529 100644 --- a/ui/web/src/components/actions/AddChargingStations.vue +++ b/ui/web/src/components/actions/AddChargingStations.vue @@ -96,6 +96,7 @@ }) .then(() => { $toast.success('Charging stations successfully added') + return refreshChargingStations() }) .catch((error: Error) => { $toast.error('Error at adding charging stations') @@ -118,7 +119,12 @@ import { getCurrentInstance, ref, watch } from 'vue' import type { UUIDv4 } from '@/types' import Button from '@/components/buttons/Button.vue' -import { convertToBoolean, randomUUID, resetToggleButtonState } from '@/composables' +import { + convertToBoolean, + randomUUID, + refreshChargingStations, + resetToggleButtonState, +} from '@/composables' const state = ref<{ autoStart: boolean @@ -140,7 +146,9 @@ const state = ref<{ template: '', }) -const templates = getCurrentInstance()?.appContext.config.globalProperties.$templates +const app = getCurrentInstance() + +const templates = app?.appContext.config.globalProperties.$templates if (templates != null) { watch(templates, () => { state.value.renderTemplates = randomUUID() diff --git a/ui/web/src/components/actions/SetSupervisionUrl.vue b/ui/web/src/components/actions/SetSupervisionUrl.vue index 4cbbd905..6406467f 100644 --- a/ui/web/src/components/actions/SetSupervisionUrl.vue +++ b/ui/web/src/components/actions/SetSupervisionUrl.vue @@ -21,6 +21,7 @@ ?.setSupervisionUrl(hashId, state.supervisionUrl) .then(() => { $toast.success('Supervision url successfully set') + return refreshChargingStations() }) .catch((error: Error) => { $toast.error('Error at setting supervision url') @@ -41,7 +42,7 @@ import { ref } from 'vue' import Button from '@/components/buttons/Button.vue' -import { resetToggleButtonState } from '@/composables' +import { refreshChargingStations, resetToggleButtonState } from '@/composables' const props = defineProps<{ chargingStationId: string diff --git a/ui/web/src/components/actions/StartTransaction.vue b/ui/web/src/components/actions/StartTransaction.vue index ac7f9230..973c0426 100644 --- a/ui/web/src/components/actions/StartTransaction.vue +++ b/ui/web/src/components/actions/StartTransaction.vue @@ -42,7 +42,13 @@ import { useRoute, useRouter } from 'vue-router' import { useToast } from 'vue-toast-notification' import Button from '@/components/buttons/Button.vue' -import { convertToInt, resetToggleButtonState, UIClient, useUIClient } from '@/composables' +import { + convertToInt, + refreshChargingStations, + resetToggleButtonState, + UIClient, + useUIClient, +} from '@/composables' import { type OCPPVersion } from '@/types' const props = defineProps<{ @@ -99,6 +105,7 @@ const handleStartTransaction = async (): Promise => { 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) diff --git a/ui/web/src/components/buttons/Button.vue b/ui/web/src/components/buttons/Button.vue index 8920855e..2b2f8c91 100644 --- a/ui/web/src/components/buttons/Button.vue +++ b/ui/web/src/components/buttons/Button.vue @@ -1,12 +1,23 @@ + + diff --git a/ui/web/src/components/buttons/StateButton.vue b/ui/web/src/components/buttons/StateButton.vue new file mode 100644 index 00000000..f3fd2d13 --- /dev/null +++ b/ui/web/src/components/buttons/StateButton.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/web/src/components/buttons/ToggleButton.vue b/ui/web/src/components/buttons/ToggleButton.vue index 694d7502..b5631975 100644 --- a/ui/web/src/components/buttons/ToggleButton.vue +++ b/ui/web/src/components/buttons/ToggleButton.vue @@ -1,6 +1,6 @@ @@ -71,6 +71,7 @@ import { useToast } from 'vue-toast-notification' import type { ConnectorStatus, OCPPVersion, Status } from '@/types' import Button from '@/components/buttons/Button.vue' +import StateButton from '@/components/buttons/StateButton.vue' import ToggleButton from '@/components/buttons/ToggleButton.vue' import { useUIClient } from '@/composables' @@ -90,66 +91,58 @@ const uiClient = useUIClient() const $toast = useToast() +const executeAction = (action: Promise, successMsg: string, errorMsg: string): void => { + action + .then(() => { + $emit('need-refresh') + return $toast.success(successMsg) + }) + .catch((error: Error) => { + $toast.error(errorMsg) + console.error(`${errorMsg}:`, error) + }) +} + const stopTransaction = (): void => { if (props.connector.transactionId == null) { $toast.error('No transaction to stop') return } - uiClient - .stopTransaction(props.hashId, { + executeAction( + uiClient.stopTransaction(props.hashId, { ocppVersion: props.ocppVersion, transactionId: props.connector.transactionId, - }) - .then(() => { - return $toast.success('Transaction successfully stopped') - }) - .catch((error: Error) => { - $toast.error('Error at stopping transaction') - console.error('Error at stopping transaction:', error) - }) + }), + 'Transaction successfully stopped', + 'Error at stopping transaction' + ) } const lockConnector = (): void => { - uiClient - .lockConnector(props.hashId, props.connectorId) - .then(() => { - return $toast.success('Connector successfully locked') - }) - .catch((error: Error) => { - $toast.error('Error at locking connector') - console.error('Error at locking connector:', error) - }) + executeAction( + uiClient.lockConnector(props.hashId, props.connectorId), + 'Connector successfully locked', + 'Error at locking connector' + ) } const unlockConnector = (): void => { - uiClient - .unlockConnector(props.hashId, props.connectorId) - .then(() => { - return $toast.success('Connector successfully unlocked') - }) - .catch((error: Error) => { - $toast.error('Error at unlocking connector') - console.error('Error at unlocking connector:', error) - }) + executeAction( + uiClient.unlockConnector(props.hashId, props.connectorId), + 'Connector successfully unlocked', + 'Error at unlocking connector' + ) } const startAutomaticTransactionGenerator = (): void => { - uiClient - .startAutomaticTransactionGenerator(props.hashId, props.connectorId) - .then(() => { - return $toast.success('Automatic transaction generator successfully started') - }) - .catch((error: Error) => { - $toast.error('Error at starting automatic transaction generator') - console.error('Error at starting automatic transaction generator:', error) - }) + executeAction( + uiClient.startAutomaticTransactionGenerator(props.hashId, props.connectorId), + 'Automatic transaction generator successfully started', + 'Error at starting automatic transaction generator' + ) } const stopAutomaticTransactionGenerator = (): void => { - uiClient - .stopAutomaticTransactionGenerator(props.hashId, props.connectorId) - .then(() => { - return $toast.success('Automatic transaction generator successfully stopped') - }) - .catch((error: Error) => { - $toast.error('Error at stopping automatic transaction generator') - console.error('Error at stopping automatic transaction generator:', error) - }) + executeAction( + uiClient.stopAutomaticTransactionGenerator(props.hashId, props.connectorId), + 'Automatic transaction generator successfully stopped', + 'Error at stopping automatic transaction generator' + ) } diff --git a/ui/web/src/components/charging-stations/CSData.vue b/ui/web/src/components/charging-stations/CSData.vue index db719e0e..417620da 100644 --- a/ui/web/src/components/charging-stations/CSData.vue +++ b/ui/web/src/components/charging-stations/CSData.vue @@ -31,12 +31,20 @@ {{ chargingStation.stationInfo.firmwareVersion ?? 'Ø' }} - - + + - Open Connection - - @@ -132,11 +134,13 @@