Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SPONSOR_USERS_API_SCOPES="show-medata/read show-medata/write access-requests/rea
EMAIL_SCOPES="clients/read templates/read templates/write emails/read"
FILE_UPLOAD_SCOPES="files/upload"
SPONSOR_PAGES_API_URL=https://sponsor-pages-api.dev.fnopen.com
SPONSOR_PAGES_SCOPES="page-template/read page-template/write show-page/read show-page/write"
SPONSOR_USERS_SCOPES="show-medata/read show-medata/write access-requests/read access-requests/write sponsor-users/read sponsor-users/write groups/read groups/write media-upload/write"
SCOPES="profile openid offline_access ${SPONSOR_USERS_API_SCOPES} ${PURCHASES_API_SCOPES} ${EMAIL_SCOPES} ${FILE_UPLOAD_SCOPES} ${SPONSOR_PAGES_SCOPES} ${SCOPES_BASE_REALM}/summits/delete-event ${SCOPES_BASE_REALM}/summits/write ${SCOPES_BASE_REALM}/summits/write-event ${SCOPES_BASE_REALM}/summits/read/all ${SCOPES_BASE_REALM}/summits/read ${SCOPES_BASE_REALM}/summits/publish-event ${SCOPES_BASE_REALM}/members/read ${SCOPES_BASE_REALM}/members/read/me ${SCOPES_BASE_REALM}/speakers/write ${SCOPES_BASE_REALM}/attendees/write ${SCOPES_BASE_REALM}/members/write ${SCOPES_BASE_REALM}/organizations/write ${SCOPES_BASE_REALM}/organizations/read ${SCOPES_BASE_REALM}/summits/write-presentation-materials ${SCOPES_BASE_REALM}/summits/registration-orders/update ${SCOPES_BASE_REALM}/summits/registration-orders/delete ${SCOPES_BASE_REALM}/summits/registration-orders/create/offline ${SCOPES_BASE_REALM}/summits/badge-scans/read entity-updates/publish ${SCOPES_BASE_REALM}/audit-logs/read"
GOOGLE_API_KEY=
ALLOWED_USER_GROUPS="super-admins administrators summit-front-end-administrators summit-room-administrators track-chairs-admins sponsors"
Expand Down
174 changes: 174 additions & 0 deletions src/actions/sponsor-mu-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Copyright 2018 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 {
createAction,
getRequest,
putRequest,
deleteRequest,
startLoading,
stopLoading
} from "openstack-uicore-foundation/lib/utils/actions";
import { getAccessTokenSafely } from "../utils/methods";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
DEFAULT_PER_PAGE
} from "../utils/constants";
import { snackbarErrorHandler } from "./base-actions";

export const REQUEST_SPONSOR_MEDIA_UPLOADS = "REQUEST_SPONSOR_MEDIA_UPLOADS";
export const RECEIVE_SPONSOR_MEDIA_UPLOADS = "RECEIVE_SPONSOR_MEDIA_UPLOADS";
export const REQUEST_GENERAL_MEDIA_UPLOADS = "REQUEST_GENERAL_MEDIA_UPLOADS";
export const RECEIVE_GENERAL_MEDIA_UPLOADS = "RECEIVE_GENERAL_MEDIA_UPLOADS";
export const SPONSOR_MEDIA_UPLOAD_FILE_UPLOADED =
"SPONSOR_MEDIA_UPLOAD_FILE_UPLOADED";
export const SPONSOR_MEDIA_UPLOAD_FILE_DELETED =
"SPONSOR_MEDIA_UPLOAD_FILE_DELETED";

export const getSponsorMURequests =
(
currentPage = DEFAULT_CURRENT_PAGE,
perPage = DEFAULT_PER_PAGE,
order = "id",
orderDir = DEFAULT_ORDER_DIR
) =>
async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const { currentSummit } = currentSummitState;
const summitTZ = currentSummit.time_zone.name;
const { entity: sponsor } = currentSponsorState;
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
page: currentPage,
// fields: "id,name,max_file_size,media_upload,file_type",
// relations: "media_upload,file_type",
per_page: perPage,
access_token: accessToken
};

// order
if (order != null && orderDir != null) {
const orderDirSign = orderDir === 1 ? "" : "-";
params.order = `${orderDirSign}${order}`;
}

return getRequest(
createAction(REQUEST_SPONSOR_MEDIA_UPLOADS),
createAction(RECEIVE_SPONSOR_MEDIA_UPLOADS),
`${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/custom-media-request-modules`,
snackbarErrorHandler,
{ order, orderDir, currentPage, perPage, summitTZ }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
Comment on lines 53 to 77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure loading stops on errors (use finally).

If the request rejects, stopLoading() isn’t called, leaving the UI in a loading state. Wrap the call in try/finally or use .finally(...).

✅ Suggested fix (try/finally)
-    return getRequest(
-      createAction(REQUEST_SPONSOR_MEDIA_UPLOADS),
-      createAction(RECEIVE_SPONSOR_MEDIA_UPLOADS),
-      `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/custom-media-request-modules`,
-      snackbarErrorHandler,
-      { order, orderDir, currentPage, perPage, summitTZ }
-    )(params)(dispatch).then(() => {
-      dispatch(stopLoading());
-    });
+    try {
+      return await getRequest(
+        createAction(REQUEST_SPONSOR_MEDIA_UPLOADS),
+        createAction(RECEIVE_SPONSOR_MEDIA_UPLOADS),
+        `${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/custom-media-request-modules`,
+        snackbarErrorHandler,
+        { order, orderDir, currentPage, perPage, summitTZ }
+      )(params)(dispatch);
+    } finally {
+      dispatch(stopLoading());
+    }
🤖 Prompt for AI Agents
In `@src/actions/sponsor-mu-actions.js` around lines 45 - 69, The code dispatches
startLoading() before calling getRequest but only dispatches stopLoading()
inside the .then branch so stopLoading() is never called on rejection; modify
the call that invokes getRequest(createAction(REQUEST_SPONSOR_MEDIA_UPLOADS),
createAction(RECEIVE_SPONSOR_MEDIA_UPLOADS), ...)(params)(dispatch) so that
stopLoading() is always executed by using a try/finally around an await of that
request or by appending .finally(() => dispatch(stopLoading())), keeping the
same parameters and handlers (snackbarErrorHandler) and preserving
startLoading()/stopLoading() calls.

};

export const getGeneralMURequests =
(
currentPage = DEFAULT_CURRENT_PAGE,
perPage = DEFAULT_PER_PAGE,
order = "id",
orderDir = DEFAULT_ORDER_DIR
) =>
async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const { currentSummit } = currentSummitState;
const summitTZ = currentSummit.time_zone.name;
const { entity: sponsor } = currentSponsorState;
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
page: currentPage,
// fields: "id,name,max_file_size,media_upload,file_type",
expand: "media_upload,file_type",
per_page: perPage,
access_token: accessToken
};

// order
if (order != null && orderDir != null) {
const orderDirSign = orderDir === 1 ? "" : "-";
params.order = `${orderDirSign}${order}`;
}

return getRequest(
createAction(REQUEST_GENERAL_MEDIA_UPLOADS),
createAction(RECEIVE_GENERAL_MEDIA_UPLOADS),
`${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/managed-media-request-modules`,
snackbarErrorHandler,
{ order, orderDir, currentPage, perPage, summitTZ }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};

export const uploadFileForSponsorMU =
(pageId, moduleId, fileObj) => async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const { currentSummit } = currentSummitState;
const { entity: sponsor } = currentSponsorState;
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
access_token: accessToken
};

return putRequest(
null,
createAction("DUMMY_ACTION"),
`${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/available-pages/${pageId}/modules/${moduleId}/file`,
fileObj,
snackbarErrorHandler
)(params)(dispatch)
.then(({ response }) => {
dispatch(
createAction(SPONSOR_MEDIA_UPLOAD_FILE_UPLOADED)({
...response,
moduleId
})
);
})
.finally(() => {
dispatch(stopLoading());
});
};

export const removeFileForSponsorMU =
(pageId, moduleId) => async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const { currentSummit } = currentSummitState;
const { entity: sponsor } = currentSponsorState;
const accessToken = await getAccessTokenSafely();

const params = {
access_token: accessToken
};

return deleteRequest(
null,
createAction(SPONSOR_MEDIA_UPLOAD_FILE_DELETED)({ moduleId }),
`${window.SPONSOR_PAGES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/available-pages/${pageId}/modules/${moduleId}/file`,
null,
snackbarErrorHandler
)(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
165 changes: 165 additions & 0 deletions src/components/upload-dialog/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, { useState } from "react";
import {
Box,
Button,
Dialog,
DialogContent,
DialogTitle,
Divider,
IconButton,
Typography
} from "@mui/material";
import PropTypes from "prop-types";
import UploadInputV2 from "openstack-uicore-foundation/lib/components/inputs/upload-input-v2";
import T from "i18n-react/dist/i18n-react";
import CloseIcon from "@mui/icons-material/Close";
import NoteAddIcon from "@mui/icons-material/NoteAdd";
import DeleteIcon from "@mui/icons-material/Delete";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import DialogActions from "@mui/material/DialogActions";

const MAX_PAGE_MODULE_UPLOAD_QTY = 1;

const CurrentFile = ({ file, onRemove }) => (
<Box sx={{ display: "flex", flexDirection: "row" }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 40
}}
>
<NoteAddIcon sx={{ mr: 1 }} color="primary" />
</Box>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="body2" sx={{ mb: 1 }}>
{file.filename}
</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
{file.size} {T.translate("upload_input.complete")}
</Typography>
</Box>
<IconButton aria-label="delete" onClick={onRemove}>
<DeleteIcon />
</IconButton>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 40
}}
>
<CheckCircleIcon color="success" sx={{ ml: 1 }} />
</Box>
</Box>
);

const UploadDialog = ({
name,
value,
open,
fileMeta,
maxFiles = MAX_PAGE_MODULE_UPLOAD_QTY,
onClose,
onUpload,
onRemove
}) => {
const [uploadedFile, setUploadedFile] = useState(null);

const mediaType = {
id: name,
max_size: fileMeta.max_file_size,
max_uploads_qty: maxFiles,
type: {
allowed_extensions: fileMeta?.allowed_extensions?.split(",") || []
}
};

const handleUpload = () => {
onUpload(uploadedFile);
};

const handleRemove = () => {
onRemove();
};

const canAddMore = () => (value?.length || 0) < maxFiles;

const getInputValue = () =>
value?.length > 0
? value.map((file) => ({
...file,
filename:
file.file_name ?? file.filename ?? file.file_path ?? file.file_url
}))
: [];

return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{T.translate("edit_sponsor.mu_tab.upload_input.upload_file")}
</DialogTitle>
<IconButton
aria-label="close"
onClick={onClose}
sx={(theme) => ({
position: "absolute",
right: 8,
top: 8,
color: theme.palette.grey[500]
})}
>
<CloseIcon />
</IconButton>
<Divider />
<DialogContent>
<Typography variant="body1" sx={{ mb: 2 }}>
{fileMeta.name}
</Typography>
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
{fileMeta.description}
</Typography>
{value ? (
<>
<Divider sx={{ marginLeft: -2, marginRight: -2, mb: 2 }} />
<CurrentFile file={value} onRemove={handleRemove} />
</>
) : (
<UploadInputV2
id={`media_upload_${name}`}
name={name}
onUploadComplete={setUploadedFile}
value={getInputValue()}
mediaType={mediaType}
onRemove={() => setUploadedFile(null)}
postUrl={`${window.FILE_UPLOAD_API_BASE_URL}/api/v1/files/upload`}
djsConfig={{ withCredentials: true }}
maxFiles={maxFiles}
canAdd={canAddMore()}
parallelChunkUploads
/>
)}
</DialogContent>
<DialogActions>
<Button
onClick={handleUpload}
fullWidth
disabled={!uploadedFile}
variant="contained"
>
{T.translate("edit_sponsor.mu_tab.upload_input.upload_file")}
</Button>
</DialogActions>
</Dialog>
);
};

UploadDialog.propTypes = {
open: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired
};

export default UploadDialog;
15 changes: 15 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,21 @@
"details": "Details",
"purchases": "purchases"
},
"mu_tab": {
"alert_info": "Here you can see the status of this Sponsor's Media Uploads. If an additional file upload is required, it must be requested from the specific page where it is needed.",
"media_upload": "media upload",
"sponsor_request": "Sponsor Specific Request",
"general_request": "General Media Request",
"add_on": "Add-on",
"max_size": "Max Size",
"format": "Format",
"deadline": "Deadline",
"status": "Status",
"upload_input": {
"complete": "Complete",
"upload_file": "Upload file"
}
},
"placeholders": {
"select_sponsorship": "Select a Sponsorship",
"sponsorship_type": "Start typing to choose a Tier...",
Expand Down
7 changes: 7 additions & 0 deletions src/pages/sponsors/edit-sponsor-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import SponsorCartTab from "./sponsor-cart-tab";
import SponsorFormsManageItems from "./sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items";
import { SPONSOR_TABS } from "../../utils/constants";
import SponsorPurchasesTab from "./sponsor-purchases-tab";
import SponsorMediaUploadTab from "./sponsor-media-upload-tab";

export const tabsToFragmentMap = [
"general",
Expand Down Expand Up @@ -246,8 +247,14 @@ const EditSponsorPage = (props) => {
/>
</CustomTabPanel>
<CustomTabPanel value={selectedTab} index={1}>
USERS
</CustomTabPanel>
<CustomTabPanel value={selectedTab} index={2}>
<SponsorUsersListPerSponsorPage sponsor={entity} />
</CustomTabPanel>
<CustomTabPanel value={selectedTab} index={3}>
<SponsorMediaUploadTab sponsor={entity} summitId={currentSummit.id} />
</CustomTabPanel>
Comment on lines 249 to +257
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the tab-panel index mismatch (Users and Pages are swapped).

The Users tab now renders a placeholder string while the Pages tab renders the users list, which is a user-visible functional mismatch. Ensure the Users tab renders SponsorUsersListPerSponsorPage at index 1 and the Pages tab renders its intended content at index 2 (or remove the placeholder if Pages is not implemented).

🤖 Prompt for AI Agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.
In `@src/pages/sponsors/edit-sponsor-page.js` around lines 249 - 257, The Users
and Pages tab panels are swapped: CustomTabPanel with index={1} currently
renders a "USERS" placeholder while index={2} renders the actual
SponsorUsersListPerSponsorPage; update the tab-panel children so that
CustomTabPanel value={selectedTab} index={1} renders
SponsorUsersListPerSponsorPage (the users list) and CustomTabPanel
value={selectedTab} index={2} renders the Pages content (or remove the
placeholder if Pages is not implemented); ensure you keep SponsorMediaUploadTab
at index={3} and only adjust the children of the CustomTabPanel components and
their index assignments to reflect this corrected mapping.

<CustomTabPanel value={selectedTab} index={4}>
{isNestedFormItemRoute ? (
<SponsorFormsManageItems match={match} />
Expand Down
Loading