diff --git a/src/actions/__tests__/dropbox-sync-actions.test.js b/src/actions/__tests__/dropbox-sync-actions.test.js index a18c8e93c..cde1da786 100644 --- a/src/actions/__tests__/dropbox-sync-actions.test.js +++ b/src/actions/__tests__/dropbox-sync-actions.test.js @@ -75,12 +75,12 @@ describe("dropbox sync actions", () => { store.dispatch(DropboxSyncActions.getSyncConfig()); await flushPromises(); - const actions = store.getActions(); + // DUMMY is the placeholder receive creator handed to uicore so the real + // commit flows through the summit-guarded commitDispatch — inert (no + // reducer handles it), excluded from the contract. + const actions = store.getActions().filter((a) => a.type !== "DUMMY"); expect(actions).toEqual([ { payload: {}, type: "REQUEST_SYNC_CONFIG" }, - // uicore's internal receive is a DUMMY; the real commit happens through - // the summit-guarded commitDispatch below it. - { payload: { response: {} }, type: "DUMMY" }, { payload: { response: {} }, type: "RECEIVE_SYNC_CONFIG" }, { payload: undefined, type: "STOP_LOADING" } ]); @@ -117,19 +117,18 @@ describe("dropbox sync actions", () => { ); await flushPromises(); - const actions = store.getActions(); + // DUMMY (uicore's placeholder receive) filtered — see getSyncConfig test. + const actions = store.getActions().filter((a) => a.type !== "DUMMY"); expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); // REQUEST_SYNC_CONFIG is dispatched by the thunk itself (pre-token, before // putRequest) so dropboxSyncState.loading covers the full save duration // and the Save/toggle/Rebuild disabled guards block overlapping saves. expect(actions[1]).toEqual({ payload: {}, type: "REQUEST_SYNC_CONFIG" }); - // uicore's internal receive is a DUMMY; the commit is summit-guarded. - expect(actions[2]).toEqual({ payload: { response: {} }, type: "DUMMY" }); - expect(actions[3]).toEqual({ + expect(actions[2]).toEqual({ payload: { response: {} }, type: "SYNC_CONFIG_UPDATED" }); - expect(actions[4]).toEqual({ payload: undefined, type: "STOP_LOADING" }); + expect(actions[3]).toEqual({ payload: undefined, type: "STOP_LOADING" }); expect(putRequest).toHaveBeenCalledTimes(1); }); @@ -380,8 +379,13 @@ describe("dropbox sync actions", () => { expect(postRequest).toHaveBeenCalledTimes(1); }); - test("resyncRoom dispatches START_LOADING, RESYNC_ROOM_DISPATCHED, STOP_LOADING", async () => { - store.dispatch(DropboxSyncActions.resyncRoom("Main Venue", "Room A")); + test("resyncRoom targets the materializer's integer venue/room route and dispatches the action sequence", async () => { + // The materializer route is ////. + // It previously took names, and sending names does not resolve at all — + // the request 404s and the resync silently never happens. The older + // assertions here only checked that postRequest was called, so they stayed + // green through that regression; pin the URL itself. + store.dispatch(DropboxSyncActions.resyncRoom(12, 345)); await flushPromises(); const actions = store.getActions(); @@ -391,7 +395,11 @@ describe("dropbox sync actions", () => { type: "RESYNC_ROOM_DISPATCHED" }); expect(actions[2]).toEqual({ payload: undefined, type: "STOP_LOADING" }); - expect(postRequest).toHaveBeenCalled(); + expect(postRequest).toHaveBeenCalledTimes(1); + const url = postRequest.mock.calls[0][2]; + expect(url).toBe( + "https://test-api.example.com/api/v1/sync/materialize/1/12/345/" + ); }); test("getSyncConfig dispatches RECEIVE_SYNC_CONFIG with empty payload on failure", async () => { @@ -428,26 +436,30 @@ describe("dropbox sync actions", () => { expect(actions[3]).toEqual({ payload: {}, type: "SYNC_CONFIG_ERROR" }); }); - test("rebuildSync dispatches STOP_LOADING on failure", async () => { + // Both callers are fire-and-forget (location-list-page's swal .then and + // edit-location-page's bare call) — the thunk's own .catch is the only thing + // preventing an unhandled rejection when the POST fails. The overlay itself + // is not at risk: authErrorHandler dispatches stopLoading before uicore + // rejects. Pin the real contract (the promise settles), not a fabricated + // action sequence. + test("rebuildSync settles (does not reject) when the POST fails", async () => { postRequest.mockImplementation(() => mockRequestImplReject()); - store.dispatch(DropboxSyncActions.rebuildSync()); - await flushPromises(); - - const actions = store.getActions(); - expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); - expect(actions[1]).toEqual({ payload: undefined, type: "STOP_LOADING" }); + await expect( + store.dispatch(DropboxSyncActions.rebuildSync()) + ).resolves.toBeUndefined(); + // An early return before the POST would also resolve undefined — prove + // the rejection path actually ran. + expect(postRequest).toHaveBeenCalledTimes(1); }); - test("resyncRoom dispatches STOP_LOADING on failure", async () => { + test("resyncRoom settles (does not reject) when the POST fails", async () => { postRequest.mockImplementation(() => mockRequestImplReject()); - store.dispatch(DropboxSyncActions.resyncRoom("Main Venue", "Room A")); - await flushPromises(); - - const actions = store.getActions(); - expect(actions[0]).toEqual({ payload: undefined, type: "START_LOADING" }); - expect(actions[1]).toEqual({ payload: undefined, type: "STOP_LOADING" }); + await expect( + store.dispatch(DropboxSyncActions.resyncRoom(12, 345)) + ).resolves.toBeUndefined(); + expect(postRequest).toHaveBeenCalledTimes(1); }); }); @@ -520,6 +532,12 @@ describe("getAllMediaUploadTypesForAllowlist", () => { .filter((a) => a.type === "RECEIVE_ALLOWLIST_OPTIONS"); expect(receives).toHaveLength(1); expect(receives[0].payload).toEqual([{ id: 1 }, { id: 2 }]); + + // Bracketing: the run opens with the global overlay and the finally + // closes it. + const types = store.getActions().map((a) => a.type); + expect(types[0]).toBe("START_LOADING"); + expect(types[types.length - 1]).toBe("STOP_LOADING"); }); it("multi page (last_page=3, page 2 delayed to resolve AFTER page 3): exactly 3 calls, ONE RECEIVE dispatch, entries concatenated in PAGE order (SDS:919)", async () => { @@ -557,16 +575,7 @@ describe("getAllMediaUploadTypesForAllowlist", () => { expect(receives[0].payload).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]); }); - it("brackets the run with global startLoading/stopLoading (happy path)", async () => { - store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); - await flushPromises(); - - const types = store.getActions().map((a) => a.type); - expect(types[0]).toBe("START_LOADING"); - expect(types[types.length - 1]).toBe("STOP_LOADING"); - }); - - it("missing/zero summit id: returns Promise.resolve() WITHOUT dispatching anything — currentSummit defaults to a truthy {id: 0} entity (current-summit-reducer DEFAULT_ENTITY), so a bare truthy check would fetch summit 0", async () => { + it("missing/zero summit id: no dispatch, no fetch, no token access — currentSummit defaults to a truthy {id: 0} entity (current-summit-reducer DEFAULT_ENTITY), so a bare truthy check would fetch summit 0", async () => { store = mockStore({ currentSummitState: { currentSummit: { id: 0 } } }); @@ -724,40 +733,6 @@ describe("getAllMediaUploadTypesForAllowlist", () => { expect(receives[0].payload).toEqual([{ id: "inv2" }]); }); - it("summit switched with NO newer invocation: the stale invocation's RECEIVE/ERROR are suppressed BUT its stopLoading still fires (seq-only loading tier) — the overlay is not stranded", async () => { - let summitId = 1; - const dispatched = []; - const mockGetState = () => ({ - currentSummitState: { currentSummit: { id: summitId } } - }); - const mockDispatch = jest.fn((action) => - typeof action === "function" - ? action(mockDispatch, mockGetState) - : dispatched.push(action) - ); - - let pageResolve; - getRequest.mockImplementation( - () => () => () => - new Promise((resolve) => { - pageResolve = resolve; - }) - ); - - const thunkFn = DropboxSyncActions.getAllMediaUploadTypesForAllowlist(); - thunkFn(mockDispatch, mockGetState); - await flushPromises(); // token resolves, page 1 pending - - summitId = 2; // switch summit — NO new invocation - - pageResolve({ response: { data: [{ id: 1 }], last_page: 1 } }); - await flushPromises(); - - const types = dispatched.map((a) => a.type); - expect(types).not.toContain("RECEIVE_ALLOWLIST_OPTIONS"); - expect(types).toContain("STOP_LOADING"); // isNewest=true → still fires - }); - it("superseded invocation's QUEUED fan-out pages never fire an HTTP call (count getRequest invocations) — a stale identical-key request would abort the fresh invocation's in-flight page", async () => { // last_page=12: range(2,12,1) = 11 fan-out items // pLimit(TEN): 10 start immediately (pages 2..11), page 12 queued @@ -802,53 +777,6 @@ describe("getAllMediaUploadTypesForAllowlist", () => { expect(callCount).toBe(12); // page 12 of inv 1 did NOT fire }); - it("a superseded invocation dispatches NO stopLoading after being superseded (newest-clears invariant; getRequest receives the seq-guarded dispatch, so authErrorHandler's unconditional stopLoading is also suppressed)", async () => { - let callCount = 0; - let inv1Resolve; - - getRequest.mockImplementation(() => () => () => { - callCount++; - if (callCount === 1) { - return new Promise((resolve) => { - inv1Resolve = resolve; - }); - } - return Promise.resolve({ response: { data: [], last_page: 1 } }); - }); - - store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); - await flushPromises(); // inv 1 awaiting page 1 - - store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); - await flushPromises(); // inv 2 completes - - const actionsAfterInv2 = store.getActions().length; - - inv1Resolve({ response: { data: [], last_page: 1 } }); - await flushPromises(); - - // Inv 1 dispatched no STOP_LOADING (isNewest=false) - const newActions = store.getActions().slice(actionsAfterInv2); - expect(newActions.map((a) => a.type)).not.toContain("STOP_LOADING"); - }); - - it("handler identity pin: every getRequest call is constructed with snackbarErrorHandler, and a rejected page still ends with the ERROR action (does NOT exercise snackbar delivery — the mock rejects before the handler runs)", async () => { - getRequest.mockImplementation( - () => () => () => Promise.reject(new Error("Unauthorized")) - ); - - store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); - await flushPromises(); - - expect(getRequest.mock.calls.length).toBeGreaterThan(0); - getRequest.mock.calls.forEach((call) => { - expect(call[3]).toBe(snackbarErrorHandler); - }); - - const types = store.getActions().map((a) => a.type); - expect(types).toContain("ALLOWLIST_OPTIONS_ERROR"); - }); - it("page-1 failure: REQUEST then ERROR, RECEIVE never dispatched", async () => { getRequest.mockImplementation( () => () => () => Promise.reject(new Error("network error")) @@ -881,30 +809,35 @@ describe("getAllMediaUploadTypesForAllowlist", () => { const types = store.getActions().map((a) => a.type); expect(types).not.toContain("RECEIVE_ALLOWLIST_OPTIONS"); expect(types).toContain("ALLOWLIST_OPTIONS_ERROR"); + + // Handler identity pin, here because this fixture constructs BOTH + // getRequest call sites (page 1 and the fan-out): each must be wired with + // snackbarErrorHandler. Does not exercise snackbar delivery — the mock + // rejects before any handler runs. + expect(getRequest.mock.calls.length).toBeGreaterThan(1); + getRequest.mock.calls.forEach((call) => { + expect(call[3]).toBe(snackbarErrorHandler); + }); }); - it("a rejected page never produces a RECEIVE whose payload is undefined or non-array", async () => { + it("dispatches NO media-upload-list action types, counting what the thunk hands uicore (SDS: the aggregator must not mutate mediaUploadListState) — the mock dispatches the passed action creators, so a media-upload creator wired at either getRequest site would land in the log", async () => { + // last_page: 2 exercises BOTH getRequest call sites (page 1 and the + // fan-out). The default mock discards the creator arguments, which would + // make this check blind to the exact regression it names. getRequest.mockImplementation( - () => () => () => Promise.reject(new Error("error")) + (requestActionCreator, receiveActionCreator) => () => (dispatch) => { + if (typeof requestActionCreator === "function") + dispatch(requestActionCreator({})); + if (typeof receiveActionCreator === "function") + dispatch(receiveActionCreator({ response: {} })); + return Promise.resolve({ response: { data: [], last_page: 2 } }); + } ); store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); await flushPromises(); - const receives = store - .getActions() - .filter((a) => a.type === "RECEIVE_ALLOWLIST_OPTIONS"); - expect(receives).toHaveLength(0); - // Belt-and-suspenders: any accidentally-slipped-through RECEIVE must carry an array - receives.forEach((action) => { - expect(Array.isArray(action.payload)).toBe(true); - }); - }); - - it("dispatches NO media-upload-list action types (assert the dispatched type set is disjoint from media-upload-actions' constants)", async () => { - store.dispatch(DropboxSyncActions.getAllMediaUploadTypesForAllowlist()); - await flushPromises(); - + expect(getRequest.mock.calls.length).toBeGreaterThan(1); // fan-out ran const dispatchedTypes = new Set(store.getActions().map((a) => a.type)); const mediaUploadTypes = new Set( Object.values(MediaUploadActions).filter((v) => typeof v === "string") diff --git a/src/actions/dropbox-sync-actions.js b/src/actions/dropbox-sync-actions.js index b0791857a..5dd1f7f88 100644 --- a/src/actions/dropbox-sync-actions.js +++ b/src/actions/dropbox-sync-actions.js @@ -352,38 +352,38 @@ export const rebuildSync = () => async (dispatch, getState) => { }); }; -export const resyncRoom = - (venueName, roomName) => async (dispatch, getState) => { - const { currentSummitState } = getState(); - const baseUrl = getBaseUrl(); - const summitId = currentSummitState?.currentSummit?.id; - - if (!baseUrl || !summitId) return; +// The materializer's per-room route takes integer venue/room ids +// (/api/v1/sync/materialize/{summitId}/{venueId}/{roomId}/). It previously +// took names; sending names now does not resolve at all. +export const resyncRoom = (venueId, roomId) => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const baseUrl = getBaseUrl(); + const summitId = currentSummitState?.currentSummit?.id; - const accessToken = await getAccessTokenSafely(); + if (!baseUrl || !summitId) return; - dispatch(startLoading()); + const accessToken = await getAccessTokenSafely(); - const params = { - access_token: accessToken - }; + dispatch(startLoading()); - return postRequest( - null, - createAction(RESYNC_ROOM_DISPATCHED), - `${baseUrl}/api/v1/sync/materialize/${summitId}/${encodeURIComponent( - venueName - )}/${encodeURIComponent(roomName)}/`, - null, - authErrorHandler - )(params)(dispatch) - .then(() => { - dispatch(stopLoading()); - dispatch( - showSuccessMessage(T.translate("dropbox_sync.resync_dispatched")) - ); - }) - .catch(() => { - dispatch(stopLoading()); - }); + const params = { + access_token: accessToken }; + + return postRequest( + null, + createAction(RESYNC_ROOM_DISPATCHED), + `${baseUrl}/api/v1/sync/materialize/${summitId}/${venueId}/${roomId}/`, + null, + authErrorHandler + )(params)(dispatch) + .then(() => { + dispatch(stopLoading()); + dispatch( + showSuccessMessage(T.translate("dropbox_sync.resync_dispatched")) + ); + }) + .catch(() => { + dispatch(stopLoading()); + }); +}; diff --git a/src/components/forms/__tests__/location-form.test.js b/src/components/forms/__tests__/location-form.test.js index 502bfec7e..f934fe4f6 100644 --- a/src/components/forms/__tests__/location-form.test.js +++ b/src/components/forms/__tests__/location-form.test.js @@ -1,124 +1,129 @@ import React from "react"; import { render } from "@testing-library/react"; import { screen } from "@testing-library/dom"; +import userEvent from "@testing-library/user-event"; import LocationForm from "../location-form"; import currentSummitMock from "../../../__mocks__/currentSummitMock"; -describe("LocationForm", () => { - beforeEach(() => { - const props = { - history: { - length: 8, - action: "POP", - location: { - pathname: "/app/summits/69/locations/781", - search: "", - hash: "", - key: "wh0sst" - } - }, - currentSummit: currentSummitMock, - allClasses: [ - { - name: "string", - short_name: "string", - description: "string", - type: ["External", "Internal"], - banners: "array", - order: "integer", - opening_hour: "integer", - closing_hour: "integer", - address_1: "string", - address_2: "string", - zip_code: "string", - city: "string", - state: "string", - country: "string", - website_url: "string", - lng: "string", - lat: "string", - display_on_site: "boolean", - details_page: "boolean", - location_message: "string", - images: "array", - class_name: "SummitVenue", - is_main: "boolean", - floors: "array", - rooms: "array" - }, - { - name: "string", - short_name: "string", - description: "string", - type: ["External", "Internal"], - banners: "array", - order: "integer", - opening_hour: "integer", - closing_hour: "integer", - address_1: "string", - address_2: "string", - zip_code: "string", - city: "string", - state: "string", - country: "string", - website_url: "string", - lng: "string", - lat: "string", - display_on_site: "boolean", - details_page: "boolean", - location_message: "string", - images: "array", - class_name: "SummitAirport", - capacity: "integer", - airport_type: ["International", "Domestic"] - } - ], - entity: { - id: 781, - name: "International Barcelona Convention Center", - short_name: "CCIB", +const buildProps = (overrides = {}) => { + const props = { + history: { + length: 8, + action: "POP", + location: { + pathname: "/app/summits/69/locations/781", + search: "", + hash: "", + key: "wh0sst" + } + }, + currentSummit: currentSummitMock, + allClasses: [ + { + name: "string", + short_name: "string", + description: "string", + type: ["External", "Internal"], + banners: "array", + order: "integer", + opening_hour: "integer", + closing_hour: "integer", + address_1: "string", + address_2: "string", + zip_code: "string", + city: "string", + state: "string", + country: "string", + website_url: "string", + lng: "string", + lat: "string", + display_on_site: "boolean", + details_page: "boolean", + location_message: "string", + images: "array", class_name: "SummitVenue", - description: "", - location_type: "Internal", - address_1: "Plaça de Willy Brandt, 11-14", - address_2: "", - zip_code: "08019", - city: "Sant Marti", - state: "Barcelona", - country: "ES", - website_url: "", - lng: "2.2193", - lat: "41.4088", - display_on_site: false, - details_page: false, - is_main: false, - location_message: "", - maps: [], - images: [], - rooms: [], - floors: [], - capacity: 0, - booking_link: "", - sold_out: false, - airport_type: "", - hotel_type: "", - created: 1762190581, - last_edited: 1762190581, - order: 48, - opening_hour: "", - closing_hour: "" + is_main: "boolean", + floors: "array", + rooms: "array" }, - errors: {}, - onSubmit: jest.fn(), - onMapUpdate: jest.fn(), - onMarkerDragged: jest.fn(), - onFloorDelete: jest.fn(), - onRoomDelete: jest.fn(), - onImageDelete: jest.fn(), - onMapDelete: jest.fn() - }; + { + name: "string", + short_name: "string", + description: "string", + type: ["External", "Internal"], + banners: "array", + order: "integer", + opening_hour: "integer", + closing_hour: "integer", + address_1: "string", + address_2: "string", + zip_code: "string", + city: "string", + state: "string", + country: "string", + website_url: "string", + lng: "string", + lat: "string", + display_on_site: "boolean", + details_page: "boolean", + location_message: "string", + images: "array", + class_name: "SummitAirport", + capacity: "integer", + airport_type: ["International", "Domestic"] + } + ], + entity: { + id: 781, + name: "International Barcelona Convention Center", + short_name: "CCIB", + class_name: "SummitVenue", + description: "", + location_type: "Internal", + address_1: "Plaça de Willy Brandt, 11-14", + address_2: "", + zip_code: "08019", + city: "Sant Marti", + state: "Barcelona", + country: "ES", + website_url: "", + lng: "2.2193", + lat: "41.4088", + display_on_site: false, + details_page: false, + is_main: false, + location_message: "", + maps: [], + images: [], + rooms: [], + floors: [], + capacity: 0, + booking_link: "", + sold_out: false, + airport_type: "", + hotel_type: "", + created: 1762190581, + last_edited: 1762190581, + order: 48, + opening_hour: "", + closing_hour: "" + }, + errors: {}, + onSubmit: jest.fn(), + onMapUpdate: jest.fn(), + onMarkerDragged: jest.fn(), + onFloorDelete: jest.fn(), + onRoomDelete: jest.fn(), + onImageDelete: jest.fn(), + onMapDelete: jest.fn() + }; - render(); + return { ...props, ...overrides }; +}; + +describe("LocationForm", () => { + beforeEach(() => { + render(); }); describe("IsMain? checkbox", () => { @@ -128,3 +133,35 @@ describe("LocationForm", () => { }); }); }); + +describe("LocationForm Resync Room action", () => { + // The materializer's per-room resync route takes integer venue/room ids. + // This form is handed the room id already; it previously looked the room's + // NAME back up and sent that, which cannot resolve against the route. + const VENUE_ID = 781; + const ROOM_ID = 5002; + + test("passes the venue id and room id, not their names", async () => { + const onRoomResync = jest.fn(); + const props = buildProps({ syncEnabled: true, onRoomResync }); + props.entity = { + ...props.entity, + id: VENUE_ID, + rooms: [{ id: ROOM_ID, name: "Room A", capacity: 10, floor_name: "" }] + }; + + const { container } = render(); + + // The Rooms panel is collapsed by default; open it the way a user would. + await userEvent.click(screen.getByText("edit_location.rooms")); + + // uicore's Table renders custom row actions as . + const resyncButton = container.querySelector( + "[data-tip='dropbox_sync.resync_tooltip']" + ); + expect(resyncButton).toBeInTheDocument(); + await userEvent.click(resyncButton); + + expect(onRoomResync).toHaveBeenCalledWith(VENUE_ID, ROOM_ID); + }); +}); diff --git a/src/components/forms/location-form.js b/src/components/forms/location-form.js index ff6e309b8..591968896 100644 --- a/src/components/forms/location-form.js +++ b/src/components/forms/location-form.js @@ -14,10 +14,10 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import CountryDropdown from "openstack-uicore-foundation/lib/components/inputs/country-dropdown" -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import Table from "openstack-uicore-foundation/lib/components/table" +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; +import CountryDropdown from "openstack-uicore-foundation/lib/components/inputs/country-dropdown"; +import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; +import Table from "openstack-uicore-foundation/lib/components/table"; import Panel from "openstack-uicore-foundation/lib/components/sections/panel"; import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; import { GMap } from "openstack-uicore-foundation/lib/components/google-map"; @@ -212,11 +212,13 @@ class LocationForm extends React.Component { } handleRoomResync(roomId) { - const { entity } = this.state; - const persistedVenueName = this.props.entity.name; - const room = entity.rooms.find((r) => r.id === roomId); - if (room && this.props.onRoomResync) { - this.props.onRoomResync(persistedVenueName, room.name); + // IDs, not names: the materializer's resync route takes integer + // venue/room ids. Read the venue id off props (the persisted entity), not + // state, so an unsaved rename in the form cannot be sent as the target. A + // venue that has never been saved has id 0 and no rooms to resync. + const venueId = this.props.entity.id; + if (venueId && roomId && this.props.onRoomResync) { + this.props.onRoomResync(venueId, roomId); } } diff --git a/src/pages/locations/edit-location-page.js b/src/pages/locations/edit-location-page.js index cac98a18f..07671b88d 100644 --- a/src/pages/locations/edit-location-page.js +++ b/src/pages/locations/edit-location-page.js @@ -130,8 +130,8 @@ class EditLocationPage extends React.Component { }); } - handleRoomResync(venueName, roomName) { - this.props.resyncRoom(venueName, roomName); + handleRoomResync(venueId, roomId) { + this.props.resyncRoom(venueId, roomId); } render() {