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 ( { initialValues={item} onSubmit={handleOnSave} isSaving={isSaving} + onImageDeleted={handleRemoveImage} /> ); @@ -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;