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: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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"
SCOPES="profile openid offline_access ${SPONSOR_USERS_API_SCOPES} ${PURCHASES_API_SCOPES} ${EMAIL_SCOPES} ${FILE_UPLOAD_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"
SPONSOR_PAGES_API_URL=https://sponsor-pages-api.dev.fnopen.com
SPONSOR_PAGES_SCOPES=page-template/read page-template/write
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add quotes around the scope value for consistency.

The value should be quoted to align with other scope variables (lines 5, 16, 17, 18) and to satisfy the linter.

📝 Proposed fix
-SPONSOR_PAGES_SCOPES=page-template/read page-template/write
+SPONSOR_PAGES_SCOPES="page-template/read page-template/write"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SPONSOR_PAGES_SCOPES=page-template/read page-template/write
SPONSOR_PAGES_SCOPES="page-template/read page-template/write"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 21-21: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In @.env.example at line 21, The SPONSOR_PAGES_SCOPES environment variable value
is unquoted; update the .env.example so the SPONSOR_PAGES_SCOPES entry uses
quotes around the scope string (consistent with other scope variables like the
ones on lines with similar scope entries) to satisfy the linter and maintain
formatting consistency.

GOOGLE_API_KEY=
ALLOWED_USER_GROUPS="super-admins administrators summit-front-end-administrators summit-room-administrators track-chairs-admins sponsors"
APP_CLIENT_NAME = "openstack"
Expand Down
242 changes: 242 additions & 0 deletions src/actions/page-template-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/**
* Copyright 2024 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,
deleteRequest,
createAction,
stopLoading,
startLoading,
authErrorHandler,
escapeFilterValue
} 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, snackbarSuccessHandler } from "./base-actions";

export const ADD_PAGE_TEMPLATE = "ADD_PAGE_TEMPLATE";
export const PAGE_TEMPLATE_ADDED = "PAGE_TEMPLATE_ADDED";
export const PAGE_TEMPLATE_DELETED = "PAGE_TEMPLATE_DELETED";
export const PAGE_TEMPLATE_UPDATED = "PAGE_TEMPLATE_UPDATED";
export const RECEIVE_PAGE_TEMPLATE = "RECEIVE_PAGE_TEMPLATE";
export const RECEIVE_PAGE_TEMPLATES = "RECEIVE_PAGE_TEMPLATES";
export const REQUEST_PAGE_TEMPLATES = "REQUEST_PAGE_TEMPLATES";
export const RESET_PAGE_TEMPLATE_FORM = "RESET_PAGE_TEMPLATE_FORM";
export const UPDATE_PAGE_TEMPLATE = "UPDATE_PAGE_TEMPLATE";
export const PAGE_TEMPLATE_ARCHIVED = "PAGE_TEMPLATE_ARCHIVED";
export const PAGE_TEMPLATE_UNARCHIVED = "PAGE_TEMPLATE_UNARCHIVED";

export const getPageTemplates =
(
term = null,
page = DEFAULT_CURRENT_PAGE,
perPage = DEFAULT_PER_PAGE,
order = "id",
orderDir = DEFAULT_ORDER_DIR,
hideArchived = false
) =>
async (dispatch) => {
const accessToken = await getAccessTokenSafely();
const filter = [];

dispatch(startLoading());

if (term) {
const escapedTerm = escapeFilterValue(term);
filter.push(`name=@${escapedTerm},code=@${escapedTerm}`);
}

const params = {
page,
expand: "modules",
fields:
"id,code,name,modules,is_archived,modules.kind,modules.id,modules.content",
relations: "modules,modules.none",
per_page: perPage,
access_token: accessToken
};

if (hideArchived) filter.push("is_archived==0");

if (filter.length > 0) {
params["filter[]"] = filter;
}

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

return getRequest(
createAction(REQUEST_PAGE_TEMPLATES),
createAction(RECEIVE_PAGE_TEMPLATES),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates`,
authErrorHandler,
{ order, orderDir, page, perPage, term, hideArchived }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};

export const getPageTemplate = (formTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
access_token: accessToken,
expand: "materials,meta_fields,meta_fields.values"
};

return getRequest(
null,
createAction(RECEIVE_PAGE_TEMPLATE),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${formTemplateId}`,
authErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};

export const deletePageTemplate = (formTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();

dispatch(startLoading());

const params = {
access_token: accessToken
};

return deleteRequest(
null,
createAction(PAGE_TEMPLATE_DELETED)({ formTemplateId }),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${formTemplateId}`,
null,
authErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};

export const resetPageTemplateForm = () => (dispatch) => {
dispatch(createAction(RESET_PAGE_TEMPLATE_FORM)({}));
};

const normalizeEntity = (entity) => {
const normalizedEntity = { ...entity };

normalizedEntity.modules = [];

return normalizedEntity;
};

export const savePageTemplate = (entity) => async (dispatch, getState) => {
const accessToken = await getAccessTokenSafely();
const params = {
access_token: accessToken
};

dispatch(startLoading());

const normalizedEntity = normalizeEntity(entity);

if (entity.id) {
return putRequest(
createAction(UPDATE_PAGE_TEMPLATE),
createAction(PAGE_TEMPLATE_UPDATED),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${entity.id}`,
normalizedEntity,
snackbarErrorHandler,
entity
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("page_template_list.page_crud.page_saved")
})
);
getPageTemplates()(dispatch, getState);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
dispatch(stopLoading());
});
}

return postRequest(
createAction(ADD_PAGE_TEMPLATE),
createAction(PAGE_TEMPLATE_ADDED),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates`,
normalizedEntity,
snackbarErrorHandler,
entity
)(params)(dispatch)
.then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("page_template_list.page_crud.page_created")
})
);
getPageTemplates()(dispatch, getState);
})
.catch((err) => {
console.error(err);
})
.finally(() => {
dispatch(stopLoading());
});
};

/* ************************************** ARCHIVE ************************************** */

export const archivePageTemplate = (pageTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
const params = { access_token: accessToken };

return putRequest(
null,
createAction(PAGE_TEMPLATE_ARCHIVED),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
null,
snackbarErrorHandler
)(params)(dispatch);
};
Comment on lines +214 to +225
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing loading state management in archivePageTemplate.

Unlike other async actions (unarchivePageTemplate, deletePageTemplate, etc.), this action doesn't dispatch startLoading() / stopLoading(). This creates an inconsistent UX.

🔧 Proposed fix
 export const archivePageTemplate = (pageTemplateId) => async (dispatch) => {
   const accessToken = await getAccessTokenSafely();
   const params = { access_token: accessToken };

+  dispatch(startLoading());
+
   return putRequest(
     null,
     createAction(PAGE_TEMPLATE_ARCHIVED),
     `${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
     null,
     snackbarErrorHandler
-  )(params)(dispatch);
+  )(params)(dispatch).then(() => {
+    dispatch(stopLoading());
+  });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const archivePageTemplate = (pageTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
const params = { access_token: accessToken };
return putRequest(
null,
createAction(PAGE_TEMPLATE_ARCHIVED),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
null,
snackbarErrorHandler
)(params)(dispatch);
};
export const archivePageTemplate = (pageTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
const params = { access_token: accessToken };
dispatch(startLoading());
return putRequest(
null,
createAction(PAGE_TEMPLATE_ARCHIVED),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
null,
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
}).catch((error) => {
dispatch(stopLoading());
throw error;
});
};
🤖 Prompt for AI Agents
In @src/actions/page-template-actions.js around lines 214 - 225, The
archivePageTemplate action is missing loading state updates; update
archivePageTemplate to dispatch startLoading() before making the async
putRequest and ensure stopLoading() is dispatched after completion or on error
(use try/finally or promise .finally), keeping the existing putRequest call
(with createAction(PAGE_TEMPLATE_ARCHIVED) and snackbarErrorHandler) and
returning its result; reference the archivePageTemplate function, dispatch
startLoading() immediately and dispatch stopLoading() in the finally block so
loading state is consistent with unarchivePageTemplate/deletePageTemplate.


export const unarchivePageTemplate = (pageTemplateId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
const params = { access_token: accessToken };

dispatch(startLoading());

return deleteRequest(
null,
createAction(PAGE_TEMPLATE_UNARCHIVED)({ pageTemplateId }),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
Comment on lines +233 to +236
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Payload structure mismatch with reducer for unarchive.

The action dispatches createAction(PAGE_TEMPLATE_UNARCHIVED)({ pageTemplateId }) which creates payload = { pageTemplateId }. However, the reducer at Line 110 does const updatedFormTemplateId = payload, treating the entire payload as the ID instead of extracting pageTemplateId from it.

This will cause the unarchive to fail silently (no template will match).

Either fix the reducer (recommended - see reducer review) or change the action:

-   createAction(PAGE_TEMPLATE_UNARCHIVED)({ pageTemplateId }),
+   createAction(PAGE_TEMPLATE_UNARCHIVED)(pageTemplateId),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return deleteRequest(
null,
createAction(PAGE_TEMPLATE_UNARCHIVED)({ pageTemplateId }),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
return deleteRequest(
null,
createAction(PAGE_TEMPLATE_UNARCHIVED)(pageTemplateId),
`${window.SPONSOR_PAGES_API_URL}/api/v1/page-templates/${pageTemplateId}/archive`,
🤖 Prompt for AI Agents
In @src/actions/page-template-actions.js around lines 233 - 236, Reducer
handling PAGE_TEMPLATE_UNARCHIVED expects the payload to be the ID but the
action dispatches payload = { pageTemplateId }; update the reducer to extract
the ID from the action payload (e.g., const updatedFormTemplateId =
payload.pageTemplateId || payload) so it supports the current action shape (or
alternatively change the action to dispatch
createAction(PAGE_TEMPLATE_UNARCHIVED)(pageTemplateId)); ensure references to
PAGE_TEMPLATE_UNARCHIVED and pageTemplateId are used when locating the reducer
and action.

null,
snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ window.MARKETING_API_BASE_URL = process.env.MARKETING_API_BASE_URL;
window.EMAIL_API_BASE_URL = process.env.EMAIL_API_BASE_URL;
window.PURCHASES_API_URL = process.env.PURCHASES_API_URL;
window.SPONSOR_USERS_API_URL = process.env.SPONSOR_USERS_API_URL;
window.SPONSOR_PAGES_API_URL = process.env.SPONSOR_PAGES_API_URL;
window.FILE_UPLOAD_API_BASE_URL = process.env.FILE_UPLOAD_API_BASE_URL;
window.SIGNAGE_BASE_URL = process.env.SIGNAGE_BASE_URL;
window.INVENTORY_API_BASE_URL = process.env.INVENTORY_API_BASE_URL;
Expand Down
4 changes: 4 additions & 0 deletions src/components/menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ const getGlobalItems = () => [
{
name: "form_templates",
linkUrl: "form-templates"
},
{
name: "page_templates",
linkUrl: "page-templates"
}
]
},
Expand Down
33 changes: 32 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@
"submission_invitations": "Submission Invitations",
"sponsors_inventory": "Sponsors",
"form_templates": "Form Templates",
"inventory": "Inventory"
"inventory": "Inventory",
"page_templates": "Pages"
},
"schedule": {
"schedule": "Schedule",
Expand Down Expand Up @@ -3874,5 +3875,35 @@
"seat_type": "Select a Seat Type",
"status": "Select a Status"
}
},
"page_template_list": {
"page_templates": "Page Templates",
"alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit botton.",
"code": "Code",
Comment on lines +3879 to +3882
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in alert_info.

"botton" should be "button".

Proposed fix
   "page_template_list": {
     "page_templates": "Page Templates",
-    "alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit botton.",
+    "alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit button.",
     "code": "Code",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"page_template_list": {
"page_templates": "Page Templates",
"alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit botton.",
"code": "Code",
"page_template_list": {
"page_templates": "Page Templates",
"alert_info": "You can create or archive Pages from the list. To edit a Page click on the item's Edit button.",
"code": "Code",
🤖 Prompt for AI Agents
In @src/i18n/en.json around lines 3879 - 3882, Update the i18n entry for
page_template_list.alert_info to correct the typo: replace "botton" with
"button" in the string value for the "alert_info" key so it reads "To edit a
Page click on the item's Edit button."

"name": "Name",
"info_mod": "Info Mod",
"upload_mod": "Upload Mod",
"download_mod": "Download Mod",
"hide_archived": "Hide archived pages",
"no_pages": "No pages found.",
"add_new": "New Page",
"add_template": "Using Template",
"delete_form_template_warning": "Are you sure you want to delete form template ",
"using_duplicate": "Using Duplicate",
"add_form_template": "New Form",
"add_using_global_template": "Using Global Template",
"placeholders": {
"search": "Search"
},
"page_crud": {
"title": "Create New Page",
"add_info": "Add Info",
"add_doc": "Add Document Download",
"add_media": "Add Media Request",
"no_modules": "No modules added yet.",
"save": "Save Page",
"page_saved": "Page saved successfully.",
"page_created": "Page created successfully."
}
}
}
2 changes: 1 addition & 1 deletion src/layouts/form-template-item-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Switch, Route, withRouter } from "react-router-dom";
import T from "i18n-react/dist/i18n-react";
import { Breadcrumb } from "react-breadcrumbs";
import Restrict from "../routes/restrict";
import FormTemplateItemListPage from "../pages/sponsors_inventory/form-template-item-list-page";
import FormTemplateItemListPage from "../pages/sponsors-global/form-templates/form-template-item-list-page";
import NoMatchPage from "../pages/no-match-page";

const FormTemplateItemLayout = ({ match }) => (
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/form-template-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Switch, Route, withRouter } from "react-router-dom";
import T from "i18n-react/dist/i18n-react";
import { Breadcrumb } from "react-breadcrumbs";
import Restrict from "../routes/restrict";
import FormTemplateListPage from "../pages/sponsors_inventory/form-template-list-page";
import FormTemplateListPage from "../pages/sponsors-global/form-templates/form-template-list-page";
import FormTemplateItemLayout from "./form-template-item-layout";
import NoMatchPage from "../pages/no-match-page";

Expand Down
2 changes: 1 addition & 1 deletion src/layouts/inventory-item-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Switch, Route, withRouter } from "react-router-dom";
import T from "i18n-react/dist/i18n-react";
import { Breadcrumb } from "react-breadcrumbs";
import Restrict from "../routes/restrict";
import InventoryListPage from "../pages/sponsors_inventory/inventory-list-page";
import InventoryListPage from "../pages/sponsors-global/inventory/inventory-list-page";
import NoMatchPage from "../pages/no-match-page";

const InventoryItemLayout = ({ match }) => (
Expand Down
55 changes: 55 additions & 0 deletions src/layouts/page-template-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright 2024 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 React from "react";
import { Switch, Route, withRouter } from "react-router-dom";
import T from "i18n-react/dist/i18n-react";
import { Breadcrumb } from "react-breadcrumbs";
import Restrict from "../routes/restrict";
import NoMatchPage from "../pages/no-match-page";
import EditPageTemplatePage from "../pages/sponsors-global/page-templates/edit-page-template-page";
import PageTemplateListPage from "../pages/sponsors-global/page-templates/page-template-list-page";

const PageTemplateLayout = ({ match }) => (
<div>
<Breadcrumb
data={{
title: T.translate("page_template_list.page_templates"),
pathname: match.url
}}
/>
<Switch>
<Route
strict
exact
path={`${match.url}/new`}
component={EditPageTemplatePage}
/>
<Route
strict
exact
path={`${match.url}/:page_template_id(\\d+)`}
component={EditPageTemplatePage}
/>
<Route
strict
exact
path={`${match.url}`}
component={PageTemplateListPage}
/>
<Route component={NoMatchPage} />
</Switch>
</div>
);

export default Restrict(withRouter(PageTemplateLayout), "page-template");
Loading
Loading