diff --git a/src/actions/__tests__/sponsor-forms-actions.test.js b/src/actions/__tests__/sponsor-forms-actions.test.js
index 34bb57728..943db3ea0 100644
--- a/src/actions/__tests__/sponsor-forms-actions.test.js
+++ b/src/actions/__tests__/sponsor-forms-actions.test.js
@@ -6,13 +6,19 @@ import thunk from "redux-thunk";
import flushPromises from "flush-promises";
import {
getRequest,
- putRequest
+ postRequest,
+ putRequest,
+ deleteRequest
} from "openstack-uicore-foundation/lib/utils/actions";
import {
getSponsorForms,
normalizeFormTemplate,
normalizeSponsorCustomizedForm,
- updateFormTemplateTiers
+ updateFormTemplateTiers,
+ removeItemFile,
+ removeSponsorCustomizedFormItemImages,
+ saveSponsorFormItem,
+ updateSponsorFormItem
} from "../sponsor-forms-actions";
import * as methods from "../../utils/methods";
@@ -21,7 +27,8 @@ jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
postRequest: jest.fn(),
getRequest: jest.fn(),
- putRequest: jest.fn()
+ putRequest: jest.fn(),
+ deleteRequest: jest.fn()
}));
describe("Sponsor Forms Actions", () => {
@@ -288,4 +295,202 @@ describe("Sponsor Forms Actions", () => {
);
});
});
+
+ describe("removeItemFile", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+
+ deleteRequest.mockImplementation(
+ (requestActionCreator, receiveAction) => () => (dispatch) => {
+ if (typeof receiveAction === "function") {
+ dispatch(receiveAction({ response: {} }));
+ } else {
+ dispatch(receiveAction);
+ }
+ return Promise.resolve({ response: {} });
+ }
+ );
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("dispatches SPONSOR_FORM_ITEM_FILE_DELETED with fileId and itemId", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } }
+ });
+
+ store.dispatch(removeItemFile(7, 99, 555));
+ await flushPromises();
+
+ expect(deleteRequest).toHaveBeenCalledWith(
+ null,
+ {
+ type: "SPONSOR_FORM_ITEM_FILE_DELETED",
+ payload: { fileId: 555, itemId: 99 }
+ },
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items/99/images/555`,
+ null,
+ expect.any(Function)
+ );
+
+ const dispatched = store
+ .getActions()
+ .find((a) => a.type === "SPONSOR_FORM_ITEM_FILE_DELETED");
+ expect(dispatched.payload).toEqual({ fileId: 555, itemId: 99 });
+ });
+ });
+
+ describe("removeSponsorCustomizedFormItemImages", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+
+ deleteRequest.mockImplementation(
+ (requestActionCreator, receiveAction) => () => (dispatch) => {
+ if (typeof receiveAction === "function") {
+ dispatch(receiveAction({ response: {} }));
+ } else {
+ dispatch(receiveAction);
+ }
+ return Promise.resolve({ response: {} });
+ }
+ );
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("dispatches SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED with fileId and itemId", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } },
+ currentSponsorState: { entity: { id: 5 } }
+ });
+
+ store.dispatch(removeSponsorCustomizedFormItemImages(7, 99, 555));
+ await flushPromises();
+
+ expect(deleteRequest).toHaveBeenCalledWith(
+ null,
+ {
+ type: "SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED",
+ payload: { fileId: 555, itemId: 99 }
+ },
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/sponsors/5/sponsor-forms/7/items/99/images/555`,
+ null,
+ expect.any(Function)
+ );
+
+ const dispatched = store
+ .getActions()
+ .find((a) => a.type === "SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED");
+ expect(dispatched.payload).toEqual({ fileId: 555, itemId: 99 });
+ });
+ });
+
+ describe("saveSponsorFormItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+
+ postRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: { id: 100 } })
+ );
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("sends the images in the create request body and makes no follow-up image request", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } }
+ });
+
+ const entity = {
+ name: "Item",
+ images: [{ file_path: "data:image/png;base64,AAA" }],
+ meta_fields: []
+ };
+
+ await store.dispatch(saveSponsorFormItem(7, entity));
+ await flushPromises();
+
+ expect(postRequest).toHaveBeenCalledWith(
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items`,
+ expect.objectContaining({
+ images: [{ file_path: "data:image/png;base64,AAA" }]
+ }),
+ expect.any(Function)
+ );
+
+ // The item-create request itself now saves and associates the
+ // images — a follow-up per-image request would create duplicates.
+ const hitImagesEndpoint = postRequest.mock.calls.some(([, , url]) =>
+ url.includes("/images")
+ );
+ expect(hitImagesEndpoint).toBe(false);
+ });
+ });
+
+ describe("updateSponsorFormItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+
+ putRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: { id: 100 } })
+ );
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("sends the images in the update request body and makes no follow-up image request", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } }
+ });
+
+ const entity = {
+ id: 100,
+ name: "Item",
+ images: [{ id: 5, file_path: "https://cdn/a.png" }],
+ meta_fields: []
+ };
+
+ await store.dispatch(updateSponsorFormItem(7, entity));
+ await flushPromises();
+
+ expect(putRequest).toHaveBeenCalledWith(
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items/100`,
+ expect.objectContaining({
+ images: [{ id: 5, file_path: "https://cdn/a.png" }]
+ }),
+ expect.any(Function)
+ );
+
+ // The item-update request itself now saves and associates the
+ // images — a follow-up per-image request would create duplicates.
+ const hitImagesEndpoint = putRequest.mock.calls.some(([, , url]) =>
+ url.includes("/images")
+ );
+ expect(hitImagesEndpoint).toBe(false);
+ });
+ });
});
diff --git a/src/actions/inventory-shared-actions.js b/src/actions/inventory-shared-actions.js
index 64de746a7..fa275dc0d 100644
--- a/src/actions/inventory-shared-actions.js
+++ b/src/actions/inventory-shared-actions.js
@@ -272,13 +272,15 @@ export const deleteFile =
return deleteRequest(
null,
- createAction(settings.deletedActionName)({ fileId }),
+ createAction(settings.deletedActionName)({ fileId, ...settings.payload }),
`${settings.url}/${fileId}`,
null,
- authErrorHandler
- )(params)(dispatch).then(() => {
- dispatch(stopLoading());
- });
+ settings.errorHandler ?? authErrorHandler
+ )(params)(dispatch)
+ .catch(() => {})
+ .finally(() => {
+ dispatch(stopLoading());
+ });
};
/* ************************************ ARCHIVE ************************************ */
diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js
index 5492628e7..1d573e501 100644
--- a/src/actions/sponsor-forms-actions.js
+++ b/src/actions/sponsor-forms-actions.js
@@ -31,6 +31,7 @@ import {
getAccessTokenSafely,
normalizeSelectAllField
} from "../utils/methods";
+import { deleteFile } from "./inventory-shared-actions";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
@@ -97,7 +98,8 @@ export const SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED =
"SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED";
export const RESET_SPONSOR_FORM_MANAGED_ITEM =
"RESET_SPONSOR_FORM_MANAGED_ITEM";
-
+export const SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED =
+ "SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED";
// ITEMS
export const REQUEST_SPONSOR_FORM_ITEMS = "REQUEST_SPONSOR_FORM_ITEMS";
export const RECEIVE_SPONSOR_FORM_ITEMS = "RECEIVE_SPONSOR_FORM_ITEMS";
@@ -105,8 +107,7 @@ export const RECEIVE_SPONSOR_FORM_ITEM = "RECEIVE_SPONSOR_FORM_ITEM";
export const SPONSOR_FORM_ITEM_UPDATED = "SPONSOR_FORM_ITEM_UPDATED";
export const RESET_SPONSOR_FORM_ITEM = "RESET_SPONSOR_FORM_ITEM";
export const SPONSOR_FORM_ITEM_DELETED = "SPONSOR_FORM_ITEM_DELETED";
-export const SPONSOR_FORM_ITEM_IMAGES_UPDATED =
- "SPONSOR_FORM_ITEM_IMAGES_UPDATED";
+export const SPONSOR_FORM_ITEM_FILE_DELETED = "SPONSOR_FORM_ITEM_FILE_DELETED";
export const SPONSOR_FORM_ITEMS_ADDED = "SPONSOR_FORM_ITEMS_ADDED";
export const SPONSOR_FORM_ITEM_ARCHIVED = "SPONSOR_FORM_ITEM_ARCHIVED";
export const SPONSOR_FORM_ITEM_UNARCHIVED = "SPONSOR_FORM_ITEM_UNARCHIVED";
@@ -849,7 +850,8 @@ export const getSponsorCustomizedFormItems =
const params = {
page,
per_page: perPage,
- access_token: accessToken
+ access_token: accessToken,
+ expand: "images"
};
filter.push(`is_archived==${showArchived ? 1 : 0}`);
@@ -1231,35 +1233,19 @@ export const deleteSponsorFormItem =
});
};
-const saveItemImages =
- (formId, formItemId, images) => async (dispatch, getState) => {
+export const removeItemFile =
+ (formId, formItemId, fileId) => async (dispatch, getState) => {
const { currentSummitState } = getState();
const { currentSummit } = currentSummitState;
- const accessToken = await getAccessTokenSafely();
- const params = { access_token: accessToken };
- const promises = images.map((file) => {
- if (file.id) {
- return putRequest(
- null,
- createAction(SPONSOR_FORM_ITEM_IMAGES_UPDATED),
- `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${formItemId}/images/${file.id}`,
- file,
- authErrorHandler,
- file
- )(params)(dispatch);
- }
- return postRequest(
- null,
- createAction(SPONSOR_FORM_ITEM_IMAGES_UPDATED),
- `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${formItemId}/images`,
- file,
- authErrorHandler,
- file
- )(params)(dispatch);
- });
+ const settings = {
+ url: `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${formItemId}/images`,
+ deletedActionName: SPONSOR_FORM_ITEM_FILE_DELETED,
+ payload: { itemId: formItemId },
+ errorHandler: snackbarErrorHandler
+ };
- return Promise.all(promises);
+ return deleteFile(fileId, settings)(dispatch);
};
export const saveSponsorFormItem =
@@ -1283,27 +1269,13 @@ export const saveSponsorFormItem =
normalizedEntity,
snackbarErrorHandler
)(params)(dispatch)
- .then(({ response }) => {
- const promises = [Promise.resolve(0)];
-
- if (normalizedEntity.images?.length > 0) {
- const savingImages = saveItemImages(
- formId,
- response.id,
- normalizedEntity.images
- )(dispatch, getState);
-
- promises.push(savingImages);
- }
-
- return Promise.all(promises).then(() => {
- dispatch(
- snackbarSuccessHandler({
- title: T.translate("general.success"),
- html: T.translate("sponsor_form_item_list.edit_item.created")
- })
- );
- });
+ .then(() => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("sponsor_form_item_list.edit_item.created")
+ })
+ );
})
.finally(() => {
dispatch(stopLoading());
@@ -1332,26 +1304,12 @@ export const updateSponsorFormItem =
snackbarErrorHandler
)(params)(dispatch)
.then(() => {
- const promises = [Promise.resolve(0)];
-
- if (normalizedEntity.images?.length > 0) {
- const savingImages = saveItemImages(
- formId,
- entity.id,
- normalizedEntity.images
- )(dispatch, getState);
-
- promises.push(savingImages);
- }
-
- return Promise.all(promises).then(() => {
- dispatch(
- snackbarSuccessHandler({
- title: T.translate("general.success"),
- html: T.translate("sponsor_form_item_list.edit_item.updated")
- })
- );
- });
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("sponsor_form_item_list.edit_item.updated")
+ })
+ );
})
.catch((err) => {
throw err;
@@ -1752,3 +1710,21 @@ export const unarchiveSponsorCustomizedFormItem =
.catch(() => {})
.finally(() => dispatch(stopLoading()));
};
+
+export const removeSponsorCustomizedFormItemImages =
+ (formId, formItemId, fileId) => async (dispatch, getState) => {
+ const { currentSummitState, currentSponsorState } = getState();
+ const { currentSummit } = currentSummitState;
+ const {
+ entity: { id: sponsorId }
+ } = currentSponsorState;
+
+ const settings = {
+ url: `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${formItemId}/images`,
+ deletedActionName: SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED,
+ payload: { itemId: formItemId },
+ errorHandler: snackbarErrorHandler
+ };
+
+ return deleteFile(fileId, settings)(dispatch);
+ };
diff --git a/src/pages/sponsors-global/form-templates/form-template-item-list-page.js b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
index 1acff070a..d0fe99db9 100644
--- a/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
+++ b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
@@ -187,6 +187,10 @@ const FormTemplateItemListPage = ({
).catch(() => {})
);
+ const handleRemoveImage = (imageId) => {
+ deleteItemImage(formTemplateId, currentFormTemplateItem.id, imageId);
+ };
+
const columns = [
{
columnKey: "code",
@@ -319,7 +323,7 @@ const FormTemplateItemListPage = ({
onClose={() => setShowInventoryItemModal(false)}
onMetaFieldTypeDeleted={deleteItemMetaFieldType}
onMetaFieldTypeValueDeleted={deleteItemMetaFieldTypeValue}
- onImageDeleted={deleteItemImage}
+ onImageDeleted={handleRemoveImage}
/>
)}
diff --git a/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
index 72cb512cb..2aa82489e 100644
--- a/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
+++ b/src/pages/sponsors-global/form-templates/sponsor-inventory-popup.js
@@ -43,6 +43,7 @@ const SponsorItemDialog = ({
onSave,
onMetaFieldTypeDeleted,
onMetaFieldTypeValueDeleted,
+ onImageDeleted,
entity: initialEntity
}) => {
const [isSaving, setIsSaving] = useState(false);
@@ -93,6 +94,10 @@ const SponsorItemDialog = ({
onClose();
};
+ const handleIDeleteImage = (id) => {
+ if (id && onImageDeleted) onImageDeleted(id);
+ };
+
return (
@@ -263,6 +269,7 @@ SponsorItemDialog.propTypes = {
onSave: PropTypes.func.isRequired,
onMetaFieldTypeDeleted: PropTypes.func,
onMetaFieldTypeValueDeleted: PropTypes.func,
+ onImageDeleted: PropTypes.func,
entity: PropTypes.object
};
diff --git a/src/pages/sponsors-global/inventory/inventory-list-page.js b/src/pages/sponsors-global/inventory/inventory-list-page.js
index a3ba172f5..81fa856c2 100644
--- a/src/pages/sponsors-global/inventory/inventory-list-page.js
+++ b/src/pages/sponsors-global/inventory/inventory-list-page.js
@@ -149,6 +149,11 @@ const InventoryListPage = ({
? unarchiveInventoryItem(item)
: archiveInventoryItem(item);
+ const handleRemoveImage = (imageId) => {
+ if (!currentInventoryItem?.id) return;
+ deleteInventoryItemImage(currentInventoryItem.id, imageId);
+ };
+
const columns = [
{
columnKey: "code",
@@ -291,7 +296,7 @@ const InventoryListPage = ({
onClose={handleClose}
onMetaFieldTypeDeleted={deleteInventoryItemMetaFieldType}
onMetaFieldTypeValueDeleted={deleteInventoryItemMetaFieldTypeValue}
- onImageDeleted={deleteInventoryItemImage}
+ onImageDeleted={handleRemoveImage}
/>
)}
@@ -312,7 +317,6 @@ export default connect(mapStateToProps, {
getInventoryItem,
resetInventoryItemForm,
saveInventoryItem,
-
deleteInventoryItemImage,
deleteInventoryItemMetaFieldType,
deleteInventoryItemMetaFieldTypeValue,
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
index 26abd82d4..967ce3ca6 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-form.js
@@ -34,7 +34,7 @@ const buildInitialValues = (data) => ({ ...data });
addIssAfterDateFieldValidator();
-const SponsorFormItemForm = ({ initialValues, onSubmit, isSaving }) => {
+const SponsorFormItemForm = ({ initialValues, onSubmit, isSaving, onImageDeleted }) => {
const formik = useFormik({
initialValues: buildInitialValues(initialValues),
validationSchema: yup.object({
@@ -57,6 +57,10 @@ const SponsorFormItemForm = ({ initialValues, onSubmit, isSaving }) => {
// SCROLL TO ERROR
useScrollToError(formik);
+ const handleDeleteImage = (id) => {
+ if (id && onImageDeleted) onImageDeleted(id);
+ };
+
return (
{
id="item-image-upload"
name="images"
maxFiles={MAX_INVENTORY_IMAGES_UPLOAD_QTY}
+ onDelete={handleDeleteImage}
allowedExtensions={getFileUploadAllowedExtensions()}
/>
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-popup.js b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-popup.js
index b908a5c4d..4d2be9764 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-popup.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-item-popup.js
@@ -11,7 +11,7 @@ import {
import CloseIcon from "@mui/icons-material/Close";
import SponsorFormItemForm from "./sponsor-form-item-form";
-const SponsorFormItemPopup = ({ item, onClose, onSave }) => {
+const SponsorFormItemPopup = ({ item, onClose, onSave, onRemoveImage }) => {
const [isSaving, setIsSaving] = useState(false);
const handleClose = () => {
@@ -24,10 +24,14 @@ const SponsorFormItemPopup = ({ item, onClose, onSave }) => {
setIsSaving(true);
onSave(values)
.then(() => onClose())
- .catch(() => {})
+ .catch(() => { })
.finally(() => setIsSaving(false));
};
+ const handleRemoveImage = (imageId) => {
+ onRemoveImage(imageId);
+ };
+
return (
);
@@ -67,6 +72,7 @@ const SponsorFormItemPopup = ({ item, onClose, onSave }) => {
SponsorFormItemPopup.propTypes = {
onClose: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
+ onRemoveImage: PropTypes.func.isRequired,
item: PropTypes.object
};
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/index.js b/src/pages/sponsors/sponsor-form-item-list-page/index.js
index 260924bd8..4eb853833 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/index.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/index.js
@@ -36,7 +36,8 @@ import {
addInventoryItems,
resetSponsorFormItem,
archiveSponsorFormItem,
- unarchiveSponsorFormItem
+ unarchiveSponsorFormItem,
+ removeItemFile
} from "../../../actions/sponsor-forms-actions";
import { getInventoryItems } from "../../../actions/inventory-item-actions";
import SponsorFormItemPopup from "./components/sponsor-form-item-popup";
@@ -65,7 +66,8 @@ const SponsorFormItemListPage = ({
addInventoryItems,
resetSponsorFormItem,
archiveSponsorFormItem,
- unarchiveSponsorFormItem
+ unarchiveSponsorFormItem,
+ removeItemFile
}) => {
const [openPopup, setOpenPopup] = useState(null);
const { form_id: formId } = match.params;
@@ -129,6 +131,11 @@ const SponsorFormItemListPage = ({
);
};
+ const handleRemoveItemImage = (imageId) => {
+ if (!currentItem?.id) return;
+ removeItemFile(formId, currentItem.id, imageId);
+ };
+
const handleAddFromInventory = (itemIds) =>
addInventoryItems(formId, itemIds).then(() =>
getSponsorFormItems(
@@ -359,6 +366,7 @@ const SponsorFormItemListPage = ({
item={currentItem}
onSave={handleSaveItem}
onClose={handleClosePopup}
+ onRemoveImage={handleRemoveItemImage}
/>
)}
{openPopup === "inventory" && (
@@ -391,5 +399,6 @@ export default connect(mapStateToProps, {
resetSponsorFormItem,
getInventoryItems,
archiveSponsorFormItem,
- unarchiveSponsorFormItem
+ unarchiveSponsorFormItem,
+ removeItemFile
})(SponsorFormItemListPage);
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
index 86a48eae5..828eab25e 100644
--- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js
@@ -36,7 +36,8 @@ import {
deleteSponsorFormManagedItem,
resetSponsorFormManagedItem,
unarchiveSponsorCustomizedFormItem,
- getSponsorFormManagedItem
+ getSponsorFormManagedItem,
+ removeSponsorCustomizedFormItemImages
} from "../../../../../../../actions/sponsor-forms-actions";
import CustomAlert from "../../../../../../../components/mui/custom-alert";
import SponsorInventoryDialog from "../../../../../../sponsors-global/form-templates/sponsor-inventory-popup";
@@ -67,7 +68,8 @@ const SponsorFormsManageItems = ({
deleteSponsorFormManagedItem,
archiveSponsorCustomizedFormItem,
unarchiveSponsorCustomizedFormItem,
- getSponsorFormManagedItem
+ getSponsorFormManagedItem,
+ removeSponsorCustomizedFormItemImages
}) => {
const [openPopup, setOpenPopup] = useState(null);
@@ -200,6 +202,15 @@ const SponsorFormsManageItems = ({
);
};
+ const handleImageRemove = (imageId) => {
+ if (!currentInventoryItem?.id) return;
+ removeSponsorCustomizedFormItemImages(
+ formId,
+ currentInventoryItem.id,
+ imageId
+ );
+ };
+
const sponsorItemColumns = [
{
columnKey: "code",
@@ -382,6 +393,7 @@ const SponsorFormsManageItems = ({
entity={currentInventoryItem}
onSave={handleItemSave}
onClose={handleClose}
+ onImageDeleted={handleImageRemove}
/>
)}
@@ -409,7 +421,8 @@ export default Restrict(
deleteSponsorFormManagedItem,
getSponsorFormManagedItem,
archiveSponsorCustomizedFormItem,
- unarchiveSponsorCustomizedFormItem
+ unarchiveSponsorCustomizedFormItem,
+ removeSponsorCustomizedFormItemImages
})(SponsorFormsManageItems),
ACCESS_ROUTES.ADMIN_SPONSORS
);
diff --git a/src/reducers/sponsors/__tests__/sponsor-customized-form-items-list-reducer.test.js b/src/reducers/sponsors/__tests__/sponsor-customized-form-items-list-reducer.test.js
index 23ac56ae5..062b3ec20 100644
--- a/src/reducers/sponsors/__tests__/sponsor-customized-form-items-list-reducer.test.js
+++ b/src/reducers/sponsors/__tests__/sponsor-customized-form-items-list-reducer.test.js
@@ -1,6 +1,7 @@
import sponsorCustomizedFormItemsListReducer from "../sponsor-customized-form-items-list-reducer";
import {
RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
+ SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED,
SPONSOR_FORM_MANAGED_ITEM_UPDATED
} from "../../../actions/sponsor-forms-actions";
@@ -92,6 +93,74 @@ describe("sponsorCustomizedFormItemsListReducer", () => {
});
});
+ describe("SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED", () => {
+ it("removes the image from currentItem and its matching list item", () => {
+ const state = {
+ ...DEFAULT_STATE,
+ currentItem: {
+ ...DEFAULT_STATE.currentItem,
+ id: 1,
+ images: [{ id: 10 }, { id: 11 }]
+ },
+ items: [
+ buildItem({ id: 1, images: [{ id: 10 }, { id: 11 }] }),
+ buildItem({ id: 2, images: [{ id: 12 }] })
+ ]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10, itemId: 1 }
+ });
+
+ expect(result.currentItem.images).toEqual([{ id: 11 }]);
+ expect(result.items[0].images).toEqual([{ id: 11 }]);
+ expect(result.items[1].images).toEqual([{ id: 12 }]);
+ });
+
+ it("handles a currentItem with no images without throwing", () => {
+ const state = {
+ ...DEFAULT_STATE,
+ currentItem: { ...DEFAULT_STATE.currentItem, id: 1, images: undefined },
+ items: [buildItem({ id: 1, images: undefined })]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10, itemId: 1 }
+ });
+
+ expect(result.currentItem.images).toEqual([]);
+ });
+
+ it("leaves currentItem untouched when the deleted file belongs to a different item", () => {
+ // Regression for the race where the delete for item A resolves after
+ // the dialog switched to item B (RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM
+ // replaced currentItem in between) — only A's row should update.
+ const state = {
+ ...DEFAULT_STATE,
+ currentItem: {
+ ...DEFAULT_STATE.currentItem,
+ id: 2,
+ images: [{ id: 12 }]
+ },
+ items: [
+ buildItem({ id: 1, images: [{ id: 10 }, { id: 11 }] }),
+ buildItem({ id: 2, images: [{ id: 12 }] })
+ ]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10, itemId: 1 }
+ });
+
+ expect(result.currentItem).toEqual(state.currentItem);
+ expect(result.items[0].images).toEqual([{ id: 11 }]);
+ expect(result.items[1].images).toEqual([{ id: 12 }]);
+ });
+ });
+
describe("SPONSOR_FORM_MANAGED_ITEM_UPDATED", () => {
it("replaces the matching list item and preserves its images as-is", () => {
const images = [{ id: 20, file_url: "https://cdn/img.png" }];
diff --git a/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js b/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
index 054db2924..19e46c155 100644
--- a/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
+++ b/src/reducers/sponsors/__tests__/sponsor-form-items-list-reducer.test.js
@@ -8,6 +8,7 @@ import {
RESET_SPONSOR_FORM_ITEM,
SPONSOR_FORM_ITEM_ARCHIVED,
SPONSOR_FORM_ITEM_DELETED,
+ SPONSOR_FORM_ITEM_FILE_DELETED,
SPONSOR_FORM_ITEM_UNARCHIVED
} from "../../../actions/sponsor-forms-actions";
@@ -189,6 +190,42 @@ describe("SponsorFormItemsListReducer", () => {
}
});
});
+
+ it("maps file_url to file_path on each image - mirrors the customized-item reducer's edit-form image fix", () => {
+ const item = {
+ id: "A",
+ code: "A",
+ name: "A",
+ early_bird_rate: 100,
+ standard_rate: 100,
+ onsite_rate: 100,
+ default_quantity: "100",
+ is_archived: true,
+ images: [
+ { id: 10, file_url: "https://cdn/a.png" },
+ { id: 11, file_url: "https://cdn/b.png" }
+ ],
+ meta_fields: []
+ };
+
+ result = SponsorFormItemsListReducer(initialState, {
+ type: RECEIVE_SPONSOR_FORM_ITEM,
+ payload: { response: item }
+ });
+
+ expect(result.currentItem.images).toEqual([
+ {
+ id: 10,
+ file_url: "https://cdn/a.png",
+ file_path: "https://cdn/a.png"
+ },
+ {
+ id: 11,
+ file_url: "https://cdn/b.png",
+ file_path: "https://cdn/b.png"
+ }
+ ]);
+ });
});
describe("RESET_SPONSOR_FORM_ITEM", () => {
@@ -268,6 +305,60 @@ describe("SponsorFormItemsListReducer", () => {
});
});
+ describe("SPONSOR_FORM_ITEM_FILE_DELETED", () => {
+ it("removes the image from currentItem and its matching list item", () => {
+ const state = {
+ ...initialState,
+ currentItem: {
+ ...initialState.currentItem,
+ id: "A",
+ images: [{ id: "IMG_1" }, { id: "IMG_2" }]
+ },
+ items: [
+ { id: "A", images: [{ id: "IMG_1" }, { id: "IMG_2" }] },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]
+ };
+
+ result = SponsorFormItemsListReducer(state, {
+ type: SPONSOR_FORM_ITEM_FILE_DELETED,
+ payload: { fileId: "IMG_1", itemId: "A" }
+ });
+
+ expect(result.currentItem.images).toStrictEqual([{ id: "IMG_2" }]);
+ expect(result.items).toStrictEqual([
+ { id: "A", images: [{ id: "IMG_2" }] },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]);
+ });
+
+ it("leaves currentItem untouched when the deleted file belongs to a different item", () => {
+ const state = {
+ ...initialState,
+ currentItem: {
+ ...initialState.currentItem,
+ id: "B",
+ images: [{ id: "IMG_3" }]
+ },
+ items: [
+ { id: "A", images: [{ id: "IMG_1" }] },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]
+ };
+
+ result = SponsorFormItemsListReducer(state, {
+ type: SPONSOR_FORM_ITEM_FILE_DELETED,
+ payload: { fileId: "IMG_1", itemId: "A" }
+ });
+
+ expect(result.currentItem).toStrictEqual(state.currentItem);
+ expect(result.items).toStrictEqual([
+ { id: "A", images: [] },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]);
+ });
+ });
+
describe("SPONSOR_FORM_ITEM_ARCHIVED", () => {
it("execution", () => {
result = SponsorFormItemsListReducer(
diff --git a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
index 84c58770f..8f2ec2b94 100644
--- a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
@@ -22,7 +22,8 @@ import {
SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED,
SPONSOR_FORM_MANAGED_ITEM_UPDATED,
SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED,
- RESET_SPONSOR_FORM_MANAGED_ITEM
+ RESET_SPONSOR_FORM_MANAGED_ITEM,
+ SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED
} from "../../actions/sponsor-forms-actions";
import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions";
import { getSafePageAfterRemove } from "../../utils/methods";
@@ -119,6 +120,24 @@ const sponsorCustomizedFormItemsListReducer = (
};
return { ...state, currentItem };
}
+ case SPONSOR_CUSTOMIZED_FORM_ITEM_IMAGE_DELETED: {
+ const { fileId, itemId } = payload;
+ const currentItem =
+ state.currentItem.id === itemId
+ ? {
+ ...state.currentItem,
+ images:
+ state.currentItem.images?.filter((img) => img.id !== fileId) ??
+ []
+ }
+ : state.currentItem;
+ const items = state.items.map((item) =>
+ item.id === itemId
+ ? { ...item, images: item.images?.filter((img) => img.id !== fileId) }
+ : item
+ );
+ return { ...state, currentItem, items };
+ }
case SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED: {
const { itemId } = payload;
const items = state.items.filter((it) => it.id !== itemId);
diff --git a/src/reducers/sponsors/sponsor-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
index 21e0c7597..29869eb68 100644
--- a/src/reducers/sponsors/sponsor-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
@@ -20,6 +20,7 @@ import {
RESET_SPONSOR_FORM_ITEM,
SPONSOR_FORM_ITEM_ARCHIVED,
SPONSOR_FORM_ITEM_DELETED,
+ SPONSOR_FORM_ITEM_FILE_DELETED,
SPONSOR_FORM_ITEM_UNARCHIVED
} from "../../actions/sponsor-forms-actions";
import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions";
@@ -99,12 +100,14 @@ const sponsorFormItemsListReducer = (state = DEFAULT_STATE, action) => {
}
case RECEIVE_SPONSOR_FORM_ITEM: {
const item = payload.response;
-
const currentItem = {
...item,
- meta_fields: item.meta_fields.length > 0 ? item.meta_fields : []
+ images: (item.images || []).map((img) => ({
+ ...img,
+ file_path: img.file_url
+ })),
+ meta_fields: (item.meta_fields ?? []).length > 0 ? item.meta_fields : []
};
-
return { ...state, currentItem };
}
case RESET_SPONSOR_FORM_ITEM: {
@@ -116,6 +119,26 @@ const sponsorFormItemsListReducer = (state = DEFAULT_STATE, action) => {
return { ...state, items };
}
+ case SPONSOR_FORM_ITEM_FILE_DELETED: {
+ const { fileId, itemId } = payload;
+ const currentItem =
+ state.currentItem.id === itemId
+ ? {
+ ...state.currentItem,
+ images:
+ state.currentItem.images?.filter((img) => img.id !== fileId) ??
+ []
+ }
+ : state.currentItem;
+
+ const items = state.items.map((item) =>
+ item.id === itemId
+ ? { ...item, images: item.images?.filter((img) => img.id !== fileId) }
+ : item
+ );
+
+ return { ...state, currentItem, items };
+ }
case SPONSOR_FORM_ITEM_ARCHIVED: {
const { id: itemId } = payload.response;
const { totalCount, perPage, currentPage } = state;