--- /dev/null
+version: '0.2'
+words:
+ - DECI
+ - CENTI
+ - MILLI
+ - MILLIWATT
+ - Benoit
+ - catppuccin
+ - chargingstations
+ - ctrlr
+ - csms
+ - idtag
+ - idtags
+ - iccid
+ - imsi
+ - ocpp
+ - onconnection
+ - opencode
+ - evse
+ - evses
+ - kvar
+ - kvarh
+ - varh
+ - rfid
+ - workerset
+ - worktree
+ - dedup
+ - unpushed
+ - logform
+ - mnemonist
+ - poolifier
+ - measurand
+ - measurands
+ - mikro
+ - neostandard
+ - recurrency
+ - shutdowning
+ - VCAP
+ - workerd
+ - yxxx
+ - cppwm
+ - heartbeatinterval
+ - HEARTBEATINTERVAL
+ - websocketpinginterval
+ - WEBSOCKETPINGINTERVAL
+ - connectionurl
+ - CONNECTIONURL
+ - chargingstation
+ - CHARGINGSTATION
+ - authctrlr
+ - AUTHCTRLR
+ - recloser
+ - deauthorize
+ - Deauth
+ - DEAUTHORIZE
+ - deauthorized
+ - DEAUTHORIZED
+ - Deauthorization
+ - Selftest
+ - SECC
+ - Secc
+ - Overcurrent
+ - ocsp
+ - OCSP
+ - EMAID
+ - emaid
+ - IDTOKEN
+ - idtoken
+ - issuerkeyhash
+ - issuernamehash
+ - SRPC
+ - CALLRESULT
+ - CALLERROR
+ - CALLRESULTERROR
+ - reservability
+ - PPTP
+ - UIMCP
+ - Streamable
+ - modelcontextprotocol
+ - OCMF
+ - ocmf
+ - secp
+ - brainpool
+ - Eichrecht
+ - eichrecht
+ - signingmethod
+ - SIGNINGMETHOD
+ - encodingmethod
+ - ENCODINGMETHOD
+ - signedmeterdata
+ - SIGNEDMETERDATA
+ - fiscalmetering
+ - FISCALMETERING
+ - publickeywithsignedmetervalue
+ - PUBLICKEYWITHSIGNEDMETERVALUE
+ - sampleddatasignreadings
+ - SAMPLEDDATASIGNREADINGS
+ - focusables
+ - Focusables
+ - secret
import pluginVue from 'eslint-plugin-vue'
import { defineConfig } from 'eslint/config'
import neostandard, { plugins } from 'neostandard'
+import vueParser from 'vue-eslint-parser'
+
+const GLOB_TS = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts']
+const GLOB_TS_VUE = [...GLOB_TS, '**/*.vue']
+const GLOB_JS = ['**/*.js', '**/*.mjs', '**/*.cjs']
export default defineConfig([
{
ignores: ['**/dist/**'],
},
+
+ // Base configs
+
cspellConfigs.recommended,
{
rules: {
- '@cspell/spellchecker': [
- 'warn',
- {
- autoFix: true,
- cspell: {
- words: [
- 'DECI',
- 'CENTI',
- 'MILLI',
- 'MILLIWATT',
- 'Benoit',
- 'catppuccin',
- 'chargingstations',
- 'ctrlr',
- 'csms',
- 'idtag',
- 'idtags',
- 'iccid',
- 'imsi',
- 'ocpp',
- 'onconnection',
- 'opencode',
- 'evse',
- 'evses',
- 'kvar',
- 'kvarh',
- 'varh',
- 'rfid',
- 'workerset',
- 'worktree',
- 'dedup',
- 'unpushed',
- 'logform',
- 'mnemonist',
- 'poolifier',
- 'measurand',
- 'measurands',
- 'mikro',
- 'neostandard',
- 'recurrency',
- 'shutdowning',
- 'VCAP',
- 'workerd',
- 'yxxx',
- // OCPP 2.0.x domain terms
- 'cppwm',
- 'heartbeatinterval',
- 'HEARTBEATINTERVAL',
- 'websocketpinginterval',
- 'WEBSOCKETPINGINTERVAL',
- 'connectionurl',
- 'CONNECTIONURL',
- 'chargingstation',
- 'CHARGINGSTATION',
- 'authctrlr',
- 'AUTHCTRLR',
- 'recloser',
- 'deauthorize',
- 'Deauth',
- 'DEAUTHORIZE',
- 'deauthorized',
- 'DEAUTHORIZED',
- 'Deauthorization',
- 'Selftest',
- 'SECC',
- 'Secc',
- 'Overcurrent',
- 'ocsp',
- 'OCSP',
- 'EMAID',
- 'emaid',
- 'IDTOKEN',
- 'idtoken',
- 'issuerkeyhash',
- 'issuernamehash',
- // OCPP SRPC
- 'SRPC',
- 'CALLRESULT',
- 'CALLERROR',
- 'CALLRESULTERROR',
- 'reservability',
- // VPN protocol acronyms
- 'PPTP',
- // UI server protocol acronyms
- 'UIMCP',
- 'Streamable',
- 'modelcontextprotocol',
- // Signed meter values
- 'OCMF',
- 'ocmf',
- 'secp',
- 'brainpool',
- 'Eichrecht',
- 'eichrecht',
- 'signingmethod',
- 'SIGNINGMETHOD',
- 'encodingmethod',
- 'ENCODINGMETHOD',
- 'signedmeterdata',
- 'SIGNEDMETERDATA',
- 'fiscalmetering',
- 'FISCALMETERING',
- 'publickeywithsignedmetervalue',
- 'PUBLICKEYWITHSIGNEDMETERVALUE',
- 'sampleddatasignreadings',
- 'SAMPLEDDATASIGNREADINGS',
- // UI component terms
- 'focusables',
- 'Focusables',
- // Test credential fragments
- 'secret',
- ],
- },
- },
- ],
+ '@cspell/spellchecker': ['warn', { autoFix: true }],
},
},
js.configs.recommended,
],
},
},
+
+ // neostandard
+
+ ...neostandard({ ts: true }),
+
+ // Vue
+
...pluginVue.configs['flat/recommended'],
- {
- files: ['**/*.vue'],
- languageOptions: {
- globals: {
- localStorage: 'readonly',
- },
- parserOptions: {
- parser: '@typescript-eslint/parser',
- },
- },
- },
+
+ // TypeScript
+
...plugins['typescript-eslint'].config(
{
extends: [
...plugins['typescript-eslint'].configs.strictTypeChecked,
...plugins['typescript-eslint'].configs.stylisticTypeChecked,
],
- files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '*/**.vue'],
+ files: GLOB_TS_VUE,
languageOptions: {
parserOptions: {
+ extraFileExtensions: ['.vue'],
projectService: true,
// eslint-disable-next-line n/no-unsupported-features/node-builtins
tsconfigRootDir: import.meta.dirname,
},
},
{
- files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
+ files: GLOB_JS,
...plugins['typescript-eslint'].configs.disableTypeChecked,
}
),
+
+ // Vue parser restoration
+
+ {
+ files: ['**/*.vue'],
+ languageOptions: {
+ globals: {
+ localStorage: 'readonly',
+ },
+ parser: vueParser,
+ parserOptions: {
+ extraFileExtensions: ['.vue'],
+ parser: plugins['typescript-eslint'].parser,
+ projectService: true,
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+
+ // Perfectionist
+
perfectionist.configs['recommended-natural'],
{
files: ['**/*.vue'],
'perfectionist/sort-vue-attributes': 'off',
},
},
- ...neostandard({
- ts: true,
- }),
+
+ // Rule overrides
+
{
- files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '**/*.vue'],
+ files: GLOB_TS_VUE,
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
</template>
<script setup lang="ts">
-import { computed, defineAsyncComponent, markRaw, watch } from 'vue'
+import { type Component, computed, defineAsyncComponent, markRaw, watch } from 'vue'
import { ASYNC_COMPONENT_DELAY_MS, ASYNC_COMPONENT_TIMEOUT_MS } from '@/core/index.js'
import SkinLoadError from '@/shared/components/SkinLoadError.vue'
markRaw(
defineAsyncComponent({
delay: ASYNC_COMPONENT_DELAY_MS,
- errorComponent: SkinLoadError,
+ errorComponent: SkinLoadError as Component,
loader: () => s.loadLayout(),
- loadingComponent: SkinLoading,
+ loadingComponent: SkinLoading as Component,
timeout: ASYNC_COMPONENT_TIMEOUT_MS,
})
),
:key="state.renderAddChargingStations"
:off="
() => {
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
"
:on="
() => {
- $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS }).catch(() => undefined)
}
"
:shared="true"
clearToggleButtons()
refresh()
if ($route.name !== ROUTE_NAMES.CHARGING_STATIONS) {
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
},
onSimulatorStopped: () => {
const { formState, pending, submitForm, templates } = useAddStationsForm({
onFinally: () => {
resetToggleButtonState('add-charging-stations', true)
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
},
})
const success = await submitForm()
if (success) {
resetToggleButtonState(`${props.hashId}-set-supervision-url`, true)
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
}
</script>
const handleStartTransaction = async (): Promise<void> => {
await submitForm()
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
</script>
const emit = defineEmits<{ clicked: [status: boolean] }>()
-const id =
- props.shared === true
- ? `${SHARED_TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
- : `${TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
+const id = props.shared
+ ? `${SHARED_TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
+ : `${TOGGLE_BUTTON_KEY_PREFIX}${props.id}`
const state = ref<{ status: boolean }>({
- status: getFromLocalStorage<boolean>(id, props.status ?? false),
+ status: getFromLocalStorage<boolean>(id, props.status || false),
})
const click = (): void => {
- if (props.shared === true) {
+ if (props.shared) {
try {
const keys = Object.keys(getLocalStorage()).filter(
key => key !== id && key.startsWith(SHARED_TOGGLE_BUTTON_KEY_PREFIX)
}
}
}
- const current = 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
:id="`${hashId}-${evseId ?? 0}-${connectorId}-start-transaction`"
:off="
() => {
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
"
:on="
...(evseId != null ? { evseId: String(evseId) } : {}),
...(ocppVersion != null ? { ocppVersion } : {}),
},
- })
+ }).catch(() => undefined)
}
"
:shared="true"
:id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
:off="
() => {
- $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS })
+ $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
}
"
:on="
hashId: chargingStation.stationInfo.hashId,
chargingStationId: chargingStation.stationInfo.chargingStationId,
},
- })
+ }).catch(() => undefined)
}
"
:shared="true"
openConnection,
startStation: startChargingStation,
stopStation: stopChargingStation,
-} = useStationActions({ onRefresh: () => emit('need-refresh') })
+} = useStationActions({
+ onRefresh: () => {
+ emit('need-refresh')
+ },
+})
const hashId = computed(() => props.chargingStation.stationInfo.hashId)
</template>
<script setup lang="ts">
-// Dialog state via v-if (no URL coupling), enabling skin-independent modal interactions.
import { type OCPPVersion } from 'ui-common'
-import { defineAsyncComponent, ref, watch } from 'vue'
+import { type Component, defineAsyncComponent, ref, watch } from 'vue'
import {
ASYNC_COMPONENT_DELAY_MS,
import SimulatorBar from './components/SimulatorBar.vue'
import StationCard from './components/StationCard.vue'
+interface StartTxDialogPayload {
+ chargingStationId: string
+ connectorId: string
+ evseId?: number
+ hashId: string
+ ocppVersion?: OCPPVersion
+}
+
/**
* Creates a lazy-loaded dialog component with shared loading/error boundaries.
* @param loader - Dynamic import function for the dialog component
* @returns An async component definition with standardized loading and error states
*/
-function defineAsyncDialog (loader: () => Promise<{ default: unknown }>) {
+function defineAsyncDialog (loader: () => Promise<{ default: Component }>) {
return defineAsyncComponent({
delay: ASYNC_COMPONENT_DELAY_MS,
- errorComponent: SkinLoadError,
- loader: loader as () => Promise<{ default: import('vue').Component }>,
- loadingComponent: SkinLoading,
+ errorComponent: SkinLoadError as Component,
+ loader,
+ loadingComponent: SkinLoading as Component,
timeout: ASYNC_COMPONENT_TIMEOUT_MS,
})
}
chargingStationId: string
hashId: string
}>(null)
-const showStartTxDialog = ref<null | {
- chargingStationId: string
- connectorId: string
- evseId?: number
- hashId: string
- ocppVersion?: OCPPVersion
-}>(null)
+const showStartTxDialog = ref<null | StartTxDialogPayload>(null)
const showAuthorizeDialog = ref<null | {
chargingStationId: string
hashId: string
})
const identifier = computed(() =>
- props.evseId != null ? `${props.evseId}/${props.connectorId}` : String(props.connectorId)
+ props.evseId != null
+ ? `${String(props.evseId)}/${String(props.connectorId)}`
+ : String(props.connectorId)
)
const showSetConnectorStatus = ref(false)
const wh = props.connector.transactionEnergyActiveImportRegisterValue
if (wh == null) return '—'
if (wh >= WH_PER_KWH) return `${(wh / WH_PER_KWH).toFixed(2)} kWh`
- return `${Math.round(wh)} Wh`
+ return `${String(Math.round(wh))} Wh`
})
const toggleLock = (): void => {
const focusFirst = (): void => {
if (dialogEl.value == null) return
const focusables = collectFocusables()
- // Prefer first non-close button so the user lands on a real input.
- const target = focusables.find(el => !el.hasAttribute('data-modal-close')) ?? focusables[0]
- if (target != null) {
- target.focus()
- } else {
+ if (focusables.length === 0) {
dialogEl.value.focus()
+ return
}
+ const target = focusables.find(el => !el.hasAttribute('data-modal-close')) ?? focusables[0]
+ target.focus()
}
const handleEsc = (): void => {
onBeforeUnmount(() => {
document.body.style.overflow = ''
- previouslyFocused?.focus?.()
+ previouslyFocused?.focus()
})
</script>
* @returns The selected option's index
*/
function getSelectIndex (e: Event): number {
- // eslint-disable-next-line no-undef
return (e.target as HTMLSelectElement).selectedIndex
}
const simulatorLabel = computed(() => {
if (props.simulatorState == null) return 'Disconnected'
- const version = props.simulatorState.version != null ? ` (${props.simulatorState.version})` : ''
+ const version = props.simulatorState.version ? ` (${props.simulatorState.version})` : ''
return `${simulatorStarted.value ? 'Running' : 'Stopped'}${version}`
})
</script>
const wsOpen = computed(() => props.chargingStation.wsState === WebSocketReadyState.OPEN)
-const startedVariant = computed<'err' | 'ok'>(() =>
- props.chargingStation.started === true ? 'ok' : 'err'
-)
+const startedVariant = computed<'err' | 'ok'>(() => (props.chargingStation.started ? 'ok' : 'err'))
const wsVariant = computed(() => getWebSocketStateVariant(props.chargingStation.wsState))
const toggleStation = (): void => {
const hashId = props.chargingStation.stationInfo.hashId
- if (props.chargingStation.started === true) {
+ if (props.chargingStation.started) {
stopStation(hashId)
} else {
startStation(hashId)
station => {
if (station != null) {
formState.value.supervisionUrl = stripStationId(
- station.supervisionUrl ?? '',
- station.stationInfo.chargingStationId ?? ''
+ station.supervisionUrl,
+ station.stationInfo.chargingStationId
)
formState.value.supervisionUser = station.stationInfo.supervisionUser ?? ''
formState.value.supervisionPassword = station.stationInfo.supervisionPassword ?? ''