From 427a1dae0d838028e0926cd880e014971bf6396a Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Thu, 19 Mar 2026 21:34:20 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Add=20Dropbox=20sync=20controls=20?= =?UTF-8?q?=E2=80=94=20toggle,=20Rebuild=20All,=20Resync=20Room=20(#86b8tx?= =?UTF-8?q?6pv)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show Admin UI for Track 3 of the dropbox-materializer integration: - Sync toggle (react-switch) + Rebuild button on Location List page - Per-room resync icon in Rooms table Actions column (uicore custom action) - Redux actions/reducer hitting materializer API directly (OAuth2 via uicore) - Feature-flagged on DROPBOX_MATERIALIZER_API_BASE_URL env var Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 1 + src/actions/dropbox-sync-actions.js | 136 ++++++++++++++++++ src/app.js | 2 + src/components/forms/location-form.js | 27 +++- src/i18n/en.json | 16 +++ src/pages/locations/edit-location-page.js | 42 +++++- src/pages/locations/location-list-page.js | 99 ++++++++++++- .../locations/dropbox-sync-reducer.js | 57 ++++++++ src/store.js | 4 +- 9 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 src/actions/dropbox-sync-actions.js create mode 100644 src/reducers/locations/dropbox-sync-reducer.js diff --git a/.env.example b/.env.example index d1a92df06..2a6d9c3b8 100644 --- a/.env.example +++ b/.env.example @@ -37,5 +37,6 @@ SENTRY_PROJECT= SENTRY_TRACE_SAMPLE_RATE= SENTRY_TRACE_PROPAGATION_TARGETS= CFP_APP_BASE_URL= +DROPBOX_MATERIALIZER_API_BASE_URL=https://dropbox-materializer.ftnstg.app S3_MEDIA_UPLOADS_ENDPOINT_URL=https://fntech.sfo2.digitaloceanspaces.com S3_MEDIA_UPLOADS_BUCKET_NAME=PresentationMediaUploads_DEV \ No newline at end of file diff --git a/src/actions/dropbox-sync-actions.js b/src/actions/dropbox-sync-actions.js new file mode 100644 index 000000000..9339200e7 --- /dev/null +++ b/src/actions/dropbox-sync-actions.js @@ -0,0 +1,136 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import T from "i18n-react/dist/i18n-react"; +import { + getRequest, + putRequest, + postRequest, + createAction, + stopLoading, + startLoading, + showSuccessMessage, + authErrorHandler +} from "openstack-uicore-foundation/lib/utils/actions"; +import { getAccessTokenSafely } from "../utils/methods"; + +export const REQUEST_SYNC_CONFIG = "REQUEST_SYNC_CONFIG"; +export const RECEIVE_SYNC_CONFIG = "RECEIVE_SYNC_CONFIG"; +export const SYNC_CONFIG_UPDATED = "SYNC_CONFIG_UPDATED"; +export const REBUILD_SYNC_DISPATCHED = "REBUILD_SYNC_DISPATCHED"; +export const RESYNC_ROOM_DISPATCHED = "RESYNC_ROOM_DISPATCHED"; + +const getBaseUrl = () => window.DROPBOX_MATERIALIZER_API_BASE_URL; + +export const getSyncConfig = () => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + + if (!getBaseUrl()) return; + + const params = { + access_token: accessToken + }; + + return getRequest( + createAction(REQUEST_SYNC_CONFIG), + createAction(RECEIVE_SYNC_CONFIG), + `${getBaseUrl()}/api/sync/config/${currentSummit.id}/`, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; + +export const updateSyncConfig = (data) => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + + if (!getBaseUrl()) return; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return putRequest( + null, + createAction(SYNC_CONFIG_UPDATED), + `${getBaseUrl()}/api/sync/config/${currentSummit.id}/`, + data, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + dispatch(showSuccessMessage(T.translate("dropbox_sync.config_saved"))); + }); +}; + +export const rebuildSync = () => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + + if (!getBaseUrl()) return; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return postRequest( + null, + createAction(REBUILD_SYNC_DISPATCHED), + `${getBaseUrl()}/api/sync/rebuild/${currentSummit.id}/`, + null, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + dispatch( + showSuccessMessage(T.translate("dropbox_sync.rebuild_dispatched")) + ); + }); +}; + +export const resyncRoom = + (venueName, roomName) => async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + + if (!getBaseUrl()) return; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return postRequest( + null, + createAction(RESYNC_ROOM_DISPATCHED), + `${getBaseUrl()}/api/sync/materialize/${ + currentSummit.id + }/${encodeURIComponent(venueName)}/${encodeURIComponent(roomName)}/`, + null, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + dispatch( + showSuccessMessage(T.translate("dropbox_sync.resync_dispatched")) + ); + }); + }; diff --git a/src/app.js b/src/app.js index cb99477c3..90bb88fc1 100644 --- a/src/app.js +++ b/src/app.js @@ -100,6 +100,8 @@ window.SENTRY_TRACE_SAMPLE_RATE = process.env.SENTRY_TRACE_SAMPLE_RATE; window.SENTRY_TRACE_PROPAGATION_TARGETS = process.env.SENTRY_TRACE_PROPAGATION_TARGETS; window.CFP_APP_BASE_URL = process.env.CFP_APP_BASE_URL; +window.DROPBOX_MATERIALIZER_API_BASE_URL = + process.env.DROPBOX_MATERIALIZER_API_BASE_URL; if (exclusiveSections.hasOwnProperty(process.env.APP_CLIENT_NAME)) { window.EXCLUSIVE_SECTIONS = exclusiveSections[process.env.APP_CLIENT_NAME]; diff --git a/src/components/forms/location-form.js b/src/components/forms/location-form.js index 73402dd46..f015a6fab 100644 --- a/src/components/forms/location-form.js +++ b/src/components/forms/location-form.js @@ -52,6 +52,7 @@ class LocationForm extends React.Component { this.handleMapUpdate = this.handleMapUpdate.bind(this); this.handleMapClick = this.handleMapClick.bind(this); this.handleClearHours = this.handleClearHours.bind(this); + this.handleRoomResync = this.handleRoomResync.bind(this); } componentDidUpdate(prevProps) { @@ -212,6 +213,14 @@ class LocationForm extends React.Component { this.props.onMarkerDragged(entity); } + handleRoomResync(roomId) { + const { entity } = this.state; + const room = entity.rooms.find((r) => r.id === roomId); + if (room && this.props.onRoomResync) { + this.props.onRoomResync(entity.name, room.name); + } + } + render() { const { entity, showSection } = this.state; const { currentSummit, allClasses } = this.props; @@ -264,7 +273,17 @@ class LocationForm extends React.Component { const room_options = { actions: { edit: { onClick: this.handleRoomEdit }, - delete: { onClick: this.props.onRoomDelete } + delete: { onClick: this.props.onRoomDelete }, + custom: this.props.syncEnabled + ? [ + { + name: "resync_dropbox", + tooltip: T.translate("dropbox_sync.resync_tooltip"), + icon: , + onClick: this.handleRoomResync + } + ] + : [] } }; @@ -637,6 +656,12 @@ class LocationForm extends React.Component { title={T.translate("edit_location.rooms")} handleClick={this.toggleSection.bind(this, "rooms")} > + {this.props.syncEnabled && ( +
+ {" "} + {T.translate("dropbox_sync.resync_helper")} +
+ )} + + + + + )} +