import { ProcedureName, type RequestPayload } from 'ui-common'
import { runAction } from './action.js'
-import { buildHashIdsPayload } from './payload.js'
+import { buildHashIdsPayload, pickPresent } from './payload.js'
const parseCommaSeparatedInts = (value: string): number[] => {
const parsed = value.split(',').map(s => Number.parseInt(s.trim(), 10))
.option('--connector-ids <ids>', 'comma-separated connector IDs', parseCommaSeparatedInts)
.action(async (hashIds: string[], options: { connectorIds?: number[] }) => {
const payload: RequestPayload = {
- ...(options.connectorIds != null && { connectorIds: options.connectorIds }),
+ ...(pickPresent(options as Record<string, unknown>, ['connectorIds']) as RequestPayload),
...buildHashIdsPayload(hashIds),
}
await runAction(program, ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, payload)
.option('--connector-ids <ids>', 'comma-separated connector IDs', parseCommaSeparatedInts)
.action(async (hashIds: string[], options: { connectorIds?: number[] }) => {
const payload: RequestPayload = {
- ...(options.connectorIds != null && { connectorIds: options.connectorIds }),
+ ...(pickPresent(options as Record<string, unknown>, ['connectorIds']) as RequestPayload),
...buildHashIdsPayload(hashIds),
}
await runAction(program, ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, payload)
}
return result
}
+
+export const pickPresent = (
+ source: Record<string, unknown>,
+ keys: string[]
+): Record<string, unknown> =>
+ Object.fromEntries(keys.filter(k => source[k] != null).map(k => [k, source[k]]))
.option('--delete-config', 'delete station configuration files')
.action(async (hashIds: string[], options: { deleteConfig?: true }) => {
const payload: RequestPayload = {
- ...(options.deleteConfig != null && { deleteConfiguration: options.deleteConfig }),
+ ...(pickDefined(options as Record<string, unknown>, {
+ deleteConfig: 'deleteConfiguration',
+ }) as RequestPayload),
...buildHashIdsPayload(hashIds),
}
await runAction(program, ProcedureName.DELETE_CHARGING_STATIONS, payload)
--- /dev/null
+/** @file Unit tests for payload helper functions */
+
+import assert from 'node:assert'
+import { describe, it } from 'node:test'
+
+import { buildHashIdsPayload, pickDefined, pickPresent } from '../src/commands/payload.js'
+
+await describe('payload helpers', async () => {
+ await describe('buildHashIdsPayload', async () => {
+ await it('should return object with hashIds when array is non-empty', () => {
+ assert.deepStrictEqual(buildHashIdsPayload(['a', 'b']), { hashIds: ['a', 'b'] })
+ })
+
+ await it('should return empty object when array is empty', () => {
+ assert.deepStrictEqual(buildHashIdsPayload([]), {})
+ })
+
+ await it('should return object with single hashId', () => {
+ assert.deepStrictEqual(buildHashIdsPayload(['abc']), { hashIds: ['abc'] })
+ })
+ })
+
+ await describe('pickDefined', async () => {
+ await it('should pick and rename defined keys', () => {
+ const result = pickDefined(
+ { a: 1, b: 'hello', c: undefined },
+ { a: 'alpha', b: 'beta', c: 'gamma' }
+ )
+ assert.deepStrictEqual(result, { alpha: 1, beta: 'hello' })
+ })
+
+ await it('should skip null values', () => {
+ const result = pickDefined({ a: null, b: 2 }, { a: 'x', b: 'y' })
+ assert.deepStrictEqual(result, { y: 2 })
+ })
+
+ await it('should return empty object when no keys match', () => {
+ const result = pickDefined({ a: undefined }, { a: 'x' })
+ assert.deepStrictEqual(result, {})
+ })
+
+ await it('should return empty object for empty keyMap', () => {
+ const result = pickDefined({ a: 1 }, {})
+ assert.deepStrictEqual(result, {})
+ })
+ })
+
+ await describe('pickPresent', async () => {
+ await it('should pick keys that are present', () => {
+ const result = pickPresent({ a: 1, b: 'hello', c: undefined }, ['a', 'b', 'c'])
+ assert.deepStrictEqual(result, { a: 1, b: 'hello' })
+ })
+
+ await it('should skip null values', () => {
+ const result = pickPresent({ a: null, b: 2 }, ['a', 'b'])
+ assert.deepStrictEqual(result, { b: 2 })
+ })
+
+ await it('should return empty object when no keys match', () => {
+ const result = pickPresent({ a: undefined }, ['a'])
+ assert.deepStrictEqual(result, {})
+ })
+
+ await it('should return empty object for empty keys array', () => {
+ const result = pickPresent({ a: 1 }, [])
+ assert.deepStrictEqual(result, {})
+ })
+
+ await it('should handle keys not present in source', () => {
+ const result = pickPresent({ a: 1 }, ['a', 'b'])
+ assert.deepStrictEqual(result, { a: 1 })
+ })
+ })
+})
background-color: var(--color-bg-caption);
padding: var(--spacing-lg);
}
+
+/* ── Form inputs ───────────────────────────────────────────────────── */
+.input-url {
+ width: 100%;
+ max-width: 40rem;
+ text-align: left;
+}
<input
id="supervision-url"
v-model.trim="state.supervisionUrl"
- class="supervision-url"
+ class="input-url"
name="supervision-url"
placeholder="wss://"
type="url"
text-align: center;
}
-.supervision-url {
- width: 100%;
- max-width: 40rem;
- text-align: left;
-}
-
.template-options {
list-style: circle inside;
text-align: left;
<input
id="supervision-url"
v-model.trim="state.supervisionUrl"
- class="supervision-url"
+ class="input-url"
name="supervision-url"
placeholder="wss://"
type="url"
)
}
</script>
-
-<style scoped>
-.supervision-url {
- width: 100%;
- max-width: 40rem;
- text-align: left;
-}
-</style>
WebSocketReadyState,
} from 'ui-common'
import { computed } from 'vue'
-import { useToast } from 'vue-toast-notification'
import Button from '@/components/buttons/Button.vue'
import StateButton from '@/components/buttons/StateButton.vue'
const $uiClient = useUIClient()
-const $toast = useToast()
-
const executeAction = useExecuteAction($emit)
const startChargingStation = (): void => {
)
}
const deleteChargingStation = (): void => {
- $uiClient
- .deleteChargingStation(props.chargingStation.stationInfo.hashId)
- .then(() => {
- deleteLocalStorageByKeyPattern(props.chargingStation.stationInfo.hashId)
- $emit('need-refresh')
- return $toast.success('Charging station successfully deleted')
- })
- .catch((error: Error) => {
- $toast.error('Error at deleting charging station')
- console.error('Error at deleting charging station:', error)
- })
+ executeAction(
+ $uiClient.deleteChargingStation(props.chargingStation.stationInfo.hashId),
+ 'Charging station successfully deleted',
+ 'Error at deleting charging station',
+ {
+ onSuccess: () => {
+ deleteLocalStorageByKeyPattern(props.chargingStation.stationInfo.hashId)
+ },
+ }
+ )
}
</script>
throw new Error('templates not provided')
}
+export interface ExecuteActionCallbacks {
+ onFinally?: () => void
+ onSuccess?: () => void
+}
+
export const useExecuteAction = (emit?: (event: 'need-refresh') => void) => {
const $toast = useToast()
return (
action: Promise<unknown>,
successMsg: string,
errorMsg: string,
- onFinally?: () => void
+ callbacks?: (() => void) | ExecuteActionCallbacks
): void => {
+ const { onFinally, onSuccess } =
+ typeof callbacks === 'function'
+ ? { onFinally: callbacks, onSuccess: undefined }
+ : (callbacks ?? {})
action
.then(() => {
+ try {
+ onSuccess?.()
+ } catch (error: unknown) {
+ console.error('Error in onSuccess callback:', error)
+ }
emit?.('need-refresh')
return $toast.success(successMsg)
})
app.use(router).use(ToastPlugin).mount('#app')
}
-fetch('/config.json')
- .then(response => {
- if (!response.ok) {
+try {
+ const response = await fetch('/config.json')
+ if (!response.ok) {
+ // TODO: add code for UI notifications or other error handling logic
+ console.error('Failed to fetch app configuration')
+ } else {
+ try {
+ const config = (await response.json()) as ConfigurationData
+ await initializeApp(app, config)
+ } catch (error: unknown) {
// TODO: add code for UI notifications or other error handling logic
- console.error('Failed to fetch app configuration')
- return undefined
+ console.error('Error at deserializing JSON app configuration:', error)
}
- response
- .json()
- // eslint-disable-next-line promise/no-nesting
- .then(async config => {
- await initializeApp(app, config as ConfigurationData)
- return undefined
- })
- // eslint-disable-next-line promise/no-nesting
- .catch((error: unknown) => {
- // TODO: add code for UI notifications or other error handling logic
- console.error('Error at deserializing JSON app configuration:', error)
- })
- return undefined
- })
- .catch((error: unknown) => {
- // TODO: add code for UI notifications or other error handling logic
- console.error('Error at fetching app configuration:', error)
- })
+ }
+} catch (error: unknown) {
+ // TODO: add code for UI notifications or other error handling logic
+ console.error('Error at fetching app configuration:', error)
+}
type UUIDv4,
} from 'ui-common'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useToast } from 'vue-toast-notification'
import StateButton from '@/components/buttons/StateButton.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
UI_SERVER_CONFIGURATION_INDEX_KEY,
useChargingStations,
useConfiguration,
+ useExecuteAction,
useFetchData,
useTemplates,
useUIClient,
const $uiClient = useUIClient()
-const $toast = useToast()
+const executeAction = useExecuteAction()
const { fetch: getSimulatorState } = useFetchData(
() => $uiClient.simulatorState(),
)
const startSimulator = (): void => {
- $uiClient
- .startSimulator()
- .then(() => {
- return $toast.success('Simulator successfully started')
- })
- .finally(() => {
- getSimulatorState()
- })
- .catch((error: Error) => {
- $toast.error('Error at starting simulator')
- console.error('Error at starting simulator:', error)
- })
+ executeAction(
+ $uiClient.startSimulator(),
+ 'Simulator successfully started',
+ 'Error at starting simulator',
+ { onFinally: getSimulatorState }
+ )
}
const stopSimulator = (): void => {
- $uiClient
- .stopSimulator()
- .then(() => {
- clearChargingStations()
- return $toast.success('Simulator successfully stopped')
- })
- .finally(() => {
- getSimulatorState()
- })
- .catch((error: Error) => {
- $toast.error('Error at stopping simulator')
- console.error('Error at stopping simulator:', error)
- })
+ executeAction(
+ $uiClient.stopSimulator(),
+ 'Simulator successfully stopped',
+ 'Error at stopping simulator',
+ { onFinally: getSimulatorState, onSuccess: clearChargingStations }
+ )
}
</script>