diff --git a/src/actions/__tests__/sponsor-forms-actions.test.js b/src/actions/__tests__/sponsor-forms-actions.test.js
index 34bb57728..381d8b4ef 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,240 @@ 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("omits images from the create request body and POSTs new uploads to the images subresource", 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).toHaveBeenNthCalledWith(
+ 1,
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items`,
+ expect.not.objectContaining({ images: expect.anything() }),
+ expect.any(Function)
+ );
+
+ // The created item's id (100, from the mocked response) is used to
+ // POST the new upload to the images subresource - the only path that
+ // actually materializes the file server-side.
+ expect(postRequest).toHaveBeenNthCalledWith(
+ 2,
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items/100/images`,
+ { file_path: "data:image/png;base64,AAA" },
+ expect.any(Function),
+ { file_path: "data:image/png;base64,AAA" }
+ );
+ });
+ });
+
+ describe("updateSponsorFormItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+
+ putRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: { id: 100 } })
+ );
+ // Clear call history left by the sibling saveSponsorFormItem tests -
+ // this describe's assertions inspect postRequest's call log directly.
+ postRequest.mockClear();
+ postRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: {} })
+ );
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("omits persisted images from the update request body so they are never round-tripped", 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.not.objectContaining({ images: expect.anything() }),
+ expect.any(Function)
+ );
+
+ // The image already has an id (persisted) - it must never be resent,
+ // since the backend replaces the whole collection on update and can't
+ // preserve a cloned-from-inventory image's external id.
+ const hitImagesEndpoint = postRequest.mock.calls.some(
+ ([, , url]) => url && url.includes("/images")
+ );
+ expect(hitImagesEndpoint).toBe(false);
+ });
+
+ it("POSTs new (id-less) uploads to the images subresource after the update succeeds", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } }
+ });
+
+ const entity = {
+ id: 100,
+ name: "Item",
+ images: [
+ { id: 5, file_path: "https://cdn/a.png" },
+ { file_path: "data:image/png;base64,BBB" }
+ ],
+ meta_fields: []
+ };
+
+ await store.dispatch(updateSponsorFormItem(7, entity));
+ await flushPromises();
+
+ expect(postRequest).toHaveBeenCalledWith(
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/show-forms/7/items/100/images`,
+ { file_path: "data:image/png;base64,BBB" },
+ expect.any(Function),
+ { file_path: "data:image/png;base64,BBB" }
+ );
+ });
+ });
});
diff --git a/src/actions/inventory-shared-actions.js b/src/actions/inventory-shared-actions.js
index 69da56874..db31b94d5 100644
--- a/src/actions/inventory-shared-actions.js
+++ b/src/actions/inventory-shared-actions.js
@@ -273,13 +273,15 @@ export const deleteFile =
return deleteRequest(
null,
- createAction(settings.deletedActionName)({ fileId }),
+ createAction(settings.deletedActionName)({ fileId, ...settings.payload }),
`${settings.url}/${fileId}`,
null,
- snackbarErrorHandler
- )(params)(dispatch).then(() => {
- dispatch(stopLoading());
- });
+ settings.errorHandler ?? snackbarErrorHandler
+ )(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..43ba71a3d 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,28 +1269,20 @@ 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(() => {
+ .then(({ response }) =>
+ saveNewItemImages(
+ formId,
+ response.id,
+ entity.images
+ )(dispatch, getState).then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("sponsor_form_item_list.edit_item.created")
})
);
- });
- })
+ })
+ )
.finally(() => {
dispatch(stopLoading());
});
@@ -1331,28 +1309,20 @@ export const updateSponsorFormItem =
normalizedEntity,
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(() => {
+ .then(() =>
+ saveNewItemImages(
+ formId,
+ entity.id,
+ entity.images
+ )(dispatch, getState).then(() => {
dispatch(
snackbarSuccessHandler({
title: T.translate("general.success"),
html: T.translate("sponsor_form_item_list.edit_item.updated")
})
);
- });
- })
+ })
+ )
.catch((err) => {
throw err;
})
@@ -1439,17 +1409,19 @@ const normalizeItem = (entity) => {
meta_fields,
quantity_limit_per_show,
quantity_limit_per_sponsor,
- default_quantity,
- images
+ default_quantity
} = entity;
if (meta_fields) {
normalizedEntity.meta_fields = meta_fields.filter((mf) => !!mf.name);
}
- if (images) {
- normalizedEntity.images = images?.filter((img) => img.file_path);
- }
+ // Images are never round-tripped inline: the item add/update endpoint's
+ // nested-images path only clones the file name (no S3 copy) and, on
+ // update, replaces the whole collection - wiping cloned-from-inventory
+ // images whose id it can't preserve. New uploads are persisted separately
+ // via saveNewItemImages once the item itself is saved.
+ delete normalizedEntity.images;
if (quantity_limit_per_show === "")
delete normalizedEntity.quantity_limit_per_show;
@@ -1460,6 +1432,32 @@ const normalizeItem = (entity) => {
return normalizedEntity;
};
+const saveNewItemImages =
+ (formId, formItemId, images = []) =>
+ async (dispatch, getState) => {
+ const newImages = images.filter((img) => !img.id && img.file_path);
+
+ if (newImages.length === 0) return Promise.resolve();
+
+ const { currentSummitState } = getState();
+ const { currentSummit } = currentSummitState;
+ const accessToken = await getAccessTokenSafely();
+ const params = { access_token: accessToken };
+
+ const promises = newImages.map((file) =>
+ postRequest(
+ null,
+ createAction(SPONSOR_FORM_ITEM_UPDATED),
+ `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${formItemId}/images`,
+ file,
+ snackbarErrorHandler,
+ file
+ )(params)(dispatch)
+ );
+
+ return Promise.all(promises);
+ };
+
export const addInventoryItems =
(formId, itemIds) => async (dispatch, getState) => {
const { currentSummitState } = getState();
@@ -1752,3 +1750,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 8e4668f82..2fadffba1 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 7e337d2de..9e80401a5 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 89a1bea7d..d9570101a 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 6c656794c..3f530461a 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;