]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix(web): fetch templates on dialog open via layout composable (#1837) (#1839)
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Thu, 7 May 2026 21:33:27 +0000 (23:33 +0200)
committerGitHub <noreply@github.com>
Thu, 7 May 2026 21:33:27 +0000 (23:33 +0200)
Expose getTemplates from useLayoutData instead of duplicating the fetch
in useAddStationsForm. Each layout triggers the refresh at the right
moment:

- Modern skin: watch showAddDialog → getTemplates() on open
- Classic skin: watch route → getTemplates() on ADD_CHARGING_STATIONS

The templatesEqual guard in useAddStationsForm prevents resetting the
user's selection when the refetch returns an identical list.

Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com>
ui/web/src/shared/composables/useAddStationsForm.ts
ui/web/src/shared/composables/useLayoutData.ts
ui/web/src/skins/classic/ClassicLayout.vue
ui/web/src/skins/modern/ModernLayout.vue
ui/web/tests/unit/shared/composables/useAddStationsForm.test.ts
ui/web/tests/unit/skins/modern/Dialogs.test.ts

index f6a2057d6e0b4b522b0fe446a9dfca1e5a814c24..c299fe7daf575da53f28874d183509dd24bce67e 100644 (file)
@@ -39,8 +39,13 @@ export function useAddStationsForm (options?: { onFinally?: () => void }): {
   const formState = ref<AddStationsFormState>(makeInitialState())
   const pending = ref(false)
 
-  watch($templates, () => {
-    formState.value.renderTemplates = randomUUID()
+  watch($templates, (newTemplates, oldTemplates) => {
+    // Only regenerate the key when the template list content actually changes.
+    // This prevents destroying the user's current selection when an in-flight
+    // refresh returns the same set of templates.
+    if (!templatesEqual(newTemplates, oldTemplates)) {
+      formState.value.renderTemplates = randomUUID()
+    }
   })
 
   /** Resets form state to initial defaults. */
@@ -127,3 +132,14 @@ function makeInitialState (): AddStationsFormState {
 function nonEmpty (value: string): string | undefined {
   return value.length > 0 ? value : undefined
 }
+
+/**
+ * Returns `true` when both arrays have identical length and values in the same order.
+ * @param a - The incoming template list.
+ * @param b - The previous template list.
+ * @returns Whether the two arrays are deeply equal.
+ */
+function templatesEqual (a: string[], b: string[] | undefined): boolean {
+  if (a.length !== b?.length) return false
+  return a.every((v, i) => v === b[i])
+}
index dca3b8a7386f702a0c6e4a2faa3426611d3d01bb..e0a547ddb6403285789eef52e80e8fb408f092e9 100644 (file)
@@ -21,6 +21,8 @@ export interface LayoutData {
   getData: () => void
   /** Fetches only the simulator state. */
   getSimulatorState: () => void
+  /** Fetches only the charging station templates list. */
+  getTemplates: () => void
   /** Whether any data fetch is currently in progress. */
   loading: ComputedRef<boolean>
   /** Registers WS event listeners for open/error/close. */
@@ -136,6 +138,7 @@ export function useLayoutData (): LayoutData {
     getChargingStations,
     getData,
     getSimulatorState,
+    getTemplates,
     loading,
     // Exposed for edge cases (e.g. hot-reload); normally called via onMounted/onUnmounted.
     registerWSEventListeners,
index 316bf75233978fbb5cab2a199ef13282c797836b..395d4114fbbf57a0ad8010cc063ec67e96f918c9 100644 (file)
@@ -123,7 +123,7 @@ import CSTable from './components/charging-stations/CSTable.vue'
 import Container from './components/ClassicContainer.vue'
 
 const layoutData = useLayoutData()
-const { simulatorStarted, simulatorState, uiServerConfigurations } = layoutData
+const { getTemplates, simulatorStarted, simulatorState, uiServerConfigurations } = layoutData
 
 const startSimulatorLabel = computed(
   () =>
@@ -183,6 +183,8 @@ watch(
   name => {
     if (name === ROUTE_NAMES.CHARGING_STATIONS) {
       refresh()
+    } else if (name === ROUTE_NAMES.ADD_CHARGING_STATIONS) {
+      getTemplates()
     }
   }
 )
index 949ac6c95f1c5a6d12d9674867d720cdf86538d5..a3a501d2e7422159b96d1ef31c41e61b570311b3 100644 (file)
@@ -77,7 +77,7 @@
 <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 } from 'vue'
+import { defineAsyncComponent, ref, watch } from 'vue'
 
 import {
   ASYNC_COMPONENT_DELAY_MS,
@@ -124,7 +124,7 @@ const StartTransactionDialog = defineAsyncDialog(
 const $chargingStations = useChargingStations()
 
 const layoutData = useLayoutData()
-const { simulatorState, uiServerConfigurations } = layoutData
+const { getTemplates, simulatorState, uiServerConfigurations } = layoutData
 
 const uiServerIndex = ref(getFromLocalStorage<number>(UI_SERVER_CONFIGURATION_INDEX_KEY, 0))
 
@@ -144,6 +144,10 @@ const handleUIServerChange = (nextIndex: number): void => {
 const confirmingStopSim = ref(false)
 
 const showAddDialog = ref(false)
+
+watch(showAddDialog, open => {
+  if (open) getTemplates()
+})
 const showSetUrlDialog = ref<null | {
   chargingStationId: string
   hashId: string
index c14d00e8a52eae0d8866b87e4fe60483c90792f8..600ae5394e8203a90f2e61d1baf040fe6febb263 100644 (file)
@@ -133,6 +133,15 @@ describe('useAddStationsForm', () => {
     expect(mockAddChargingStations).toHaveBeenCalledWith('boundary.json', 0, expect.any(Object))
   })
 
+  it('should not update renderTemplates when refetch returns identical template list', async () => {
+    const { formState } = useAddStationsForm()
+    const before = formState.value.renderTemplates
+    // Assign a new array with the same content to $templates
+    mockTemplates.value = ['template1.json', 'template2.json']
+    await nextTick()
+    expect(formState.value.renderTemplates).toBe(before)
+  })
+
   it('should update renderTemplates reactively when templates ref changes', async () => {
     const { formState } = useAddStationsForm()
     const initial = formState.value.renderTemplates
index 6286f5c11ae3b27ce34f5086a89ff5f89ad6d2f9..5dfdb3a265b20a5e8e32ac48823b6f18eab9911f 100644 (file)
@@ -50,6 +50,7 @@ describe('Dialogs', () => {
      * @returns Mounted wrapper for AddStationsDialog
      */
     function mountDialog (templates = ['template-A.json', 'template-B.json']) {
+      mockClient.listTemplates.mockResolvedValue({ status: ResponseStatus.SUCCESS, templates })
       return mount(AddStationsDialog, {
         global: {
           provide: {