diff --git a/src/actions/__tests__/form-template-item-actions.test.js b/src/actions/__tests__/form-template-item-actions.test.js
new file mode 100644
index 000000000..280cfa754
--- /dev/null
+++ b/src/actions/__tests__/form-template-item-actions.test.js
@@ -0,0 +1,42 @@
+/**
+ * @jest-environment jsdom
+ */
+import configureStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import flushPromises from "flush-promises";
+import { getRequest } from "openstack-uicore-foundation/lib/utils/actions";
+import { getFormTemplateItem } from "../form-template-item-actions";
+import * as methods from "../../utils/methods";
+
+jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
+ __esModule: true,
+ ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
+ getRequest: jest.fn()
+}));
+
+describe("getFormTemplateItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("still dispatches STOP_LOADING when the request fails", async () => {
+ getRequest.mockImplementation(
+ () => () => () => Promise.reject(new Error("API error"))
+ );
+
+ const store = mockStore({});
+
+ await store.dispatch(getFormTemplateItem(123, 1)).catch(() => {});
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("STOP_LOADING");
+ });
+});
diff --git a/src/actions/__tests__/inventory-item-actions.test.js b/src/actions/__tests__/inventory-item-actions.test.js
new file mode 100644
index 000000000..80da2df86
--- /dev/null
+++ b/src/actions/__tests__/inventory-item-actions.test.js
@@ -0,0 +1,42 @@
+/**
+ * @jest-environment jsdom
+ */
+import configureStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import flushPromises from "flush-promises";
+import { getRequest } from "openstack-uicore-foundation/lib/utils/actions";
+import { getInventoryItem } from "../inventory-item-actions";
+import * as methods from "../../utils/methods";
+
+jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
+ __esModule: true,
+ ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
+ getRequest: jest.fn()
+}));
+
+describe("getInventoryItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("still dispatches STOP_LOADING when the request fails", async () => {
+ getRequest.mockImplementation(
+ () => () => () => Promise.reject(new Error("API error"))
+ );
+
+ const store = mockStore({});
+
+ await store.dispatch(getInventoryItem(1)).catch(() => {});
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("STOP_LOADING");
+ });
+});
diff --git a/src/actions/__tests__/inventory-shared-actions.test.js b/src/actions/__tests__/inventory-shared-actions.test.js
new file mode 100644
index 000000000..e26afb3b2
--- /dev/null
+++ b/src/actions/__tests__/inventory-shared-actions.test.js
@@ -0,0 +1,43 @@
+import { deleteRequest } from "openstack-uicore-foundation/lib/utils/actions";
+import { deleteFile } from "../inventory-shared-actions";
+import * as methods from "../../utils/methods";
+
+jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
+ __esModule: true,
+ ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
+ deleteRequest: jest.fn()
+}));
+
+describe("deleteFile", () => {
+ const dispatch = jest.fn();
+ const settings = {
+ url: "http://test-api/images",
+ deletedActionName: "SOME_FILE_DELETED"
+ };
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("resolves true when the delete request succeeds", async () => {
+ deleteRequest.mockImplementation(() => () => () => Promise.resolve());
+
+ const result = await deleteFile(10, settings)(dispatch);
+
+ expect(result).toBe(true);
+ });
+
+ it("resolves false (without rejecting) when the delete request fails", async () => {
+ deleteRequest.mockImplementation(
+ () => () => () => Promise.reject(new Error("boom"))
+ );
+
+ const result = await deleteFile(10, settings)(dispatch);
+
+ expect(result).toBe(false);
+ });
+});
diff --git a/src/actions/__tests__/sponsor-forms-actions.test.js b/src/actions/__tests__/sponsor-forms-actions.test.js
index 34bb57728..346693c7c 100644
--- a/src/actions/__tests__/sponsor-forms-actions.test.js
+++ b/src/actions/__tests__/sponsor-forms-actions.test.js
@@ -6,13 +6,22 @@ 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,
+ saveSponsorFormManagedItem,
+ getSponsorFormItem,
+ getSponsorFormManagedItem
} from "../sponsor-forms-actions";
import * as methods from "../../utils/methods";
@@ -21,7 +30,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 +298,442 @@ 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_url: "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_url: "https://cdn/a.png" },
+ { file_path: "data:image/png;base64,BBB" }
+ ],
+ meta_fields: []
+ };
+
+ await store.dispatch(updateSponsorFormItem(7, entity));
+ await flushPromises();
+
+ const imagesCalls = postRequest.mock.calls.filter(
+ ([, , url]) => url && url.includes("/images")
+ );
+ expect(imagesCalls).toHaveLength(1);
+ 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" }
+ );
+
+ // The received-action creator must produce SPONSOR_FORM_ITEM_IMAGE_ADDED
+ // carrying {response, itemId} - not the item-shaped SPONSOR_FORM_ITEM_UPDATED,
+ // which a reducer could misread as an item and corrupt an unrelated one.
+ const [, receivedActionCreator] = imagesCalls[0];
+ const uploadedImage = { id: 55, file_url: "https://cdn/new.png" };
+ expect(receivedActionCreator({ response: uploadedImage })).toEqual({
+ type: "SPONSOR_FORM_ITEM_IMAGE_ADDED",
+ payload: { response: uploadedImage, itemId: 100 }
+ });
+ });
+ });
+
+ describe("saveSponsorFormManagedItem", () => {
+ const mockStore = configureStore([thunk]);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+ putRequest.mockClear();
+ putRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: {} })
+ );
+ postRequest.mockClear();
+ postRequest.mockImplementation(
+ () => () => () => Promise.resolve({ response: { id: 200 } })
+ );
+ });
+
+ afterEach(() => jest.restoreAllMocks());
+
+ it("omits persisted images from the managed-item update request body", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } },
+ currentSponsorState: { entity: { id: 7 } }
+ });
+
+ const entity = {
+ id: 100,
+ name: "Item",
+ images: [{ id: 5, file_url: "https://cdn/a.png" }],
+ meta_fields: []
+ };
+
+ await store.dispatch(saveSponsorFormManagedItem(9, entity));
+ await flushPromises();
+
+ expect(putRequest).toHaveBeenCalledWith(
+ expect.any(Function),
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/sponsors/7/sponsor-forms/9/items/100`,
+ expect.not.objectContaining({ images: expect.anything() }),
+ expect.any(Function),
+ entity
+ );
+
+ // The image already has an id (persisted) - it must never be resent,
+ // since the update endpoint replaces the whole collection.
+ 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 managed-item update succeeds", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } },
+ currentSponsorState: { entity: { id: 7 } }
+ });
+
+ const entity = {
+ id: 100,
+ name: "Item",
+ images: [
+ { id: 5, file_url: "https://cdn/a.png" },
+ { file_path: "data:image/png;base64,CCC" }
+ ],
+ meta_fields: []
+ };
+
+ await store.dispatch(saveSponsorFormManagedItem(9, entity));
+ await flushPromises();
+
+ expect(postRequest).toHaveBeenCalledWith(
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/sponsors/7/sponsor-forms/9/items/100/images`,
+ { file_path: "data:image/png;base64,CCC" },
+ expect.any(Function),
+ { file_path: "data:image/png;base64,CCC" }
+ );
+
+ // Must be its own SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED type, not the
+ // item-shaped SPONSOR_FORM_MANAGED_ITEM_UPDATED - that reducer case
+ // reads payload.response as an item and would corrupt whichever
+ // unrelated list item happens to share the uploaded image's id.
+ const [, receivedActionCreator] = postRequest.mock.calls.find(
+ ([, , url]) => url && url.includes("/images")
+ );
+ const uploadedImage = { id: 55, file_url: "https://cdn/new.png" };
+ expect(receivedActionCreator({ response: uploadedImage })).toEqual({
+ type: "SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED",
+ payload: { response: uploadedImage, itemId: 100 }
+ });
+ });
+
+ it("omits images from the managed-item create request body and POSTs new uploads using the created item's id", async () => {
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } },
+ currentSponsorState: { entity: { id: 7 } }
+ });
+
+ const entity = {
+ name: "Item",
+ images: [{ file_path: "data:image/png;base64,DDD" }],
+ meta_fields: []
+ };
+
+ await store.dispatch(saveSponsorFormManagedItem(9, entity));
+ await flushPromises();
+
+ expect(postRequest).toHaveBeenNthCalledWith(
+ 1,
+ expect.any(Function),
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/sponsors/7/sponsor-forms/9/items`,
+ expect.not.objectContaining({ images: expect.anything() }),
+ expect.any(Function),
+ entity
+ );
+
+ expect(postRequest).toHaveBeenNthCalledWith(
+ 2,
+ null,
+ expect.any(Function),
+ `${window.PURCHASES_API_URL}/api/v1/summits/42/sponsors/7/sponsor-forms/9/items/200/images`,
+ { file_path: "data:image/png;base64,DDD" },
+ expect.any(Function),
+ { file_path: "data:image/png;base64,DDD" }
+ );
+ });
+ });
+
+ describe("getSponsorFormItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("still dispatches STOP_LOADING when the request fails", async () => {
+ getRequest.mockImplementation(
+ () => () => () => Promise.reject(new Error("API error"))
+ );
+
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } }
+ });
+
+ await store.dispatch(getSponsorFormItem(7, 100)).catch(() => {});
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("STOP_LOADING");
+ });
+ });
+
+ describe("getSponsorFormManagedItem", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockReturnValue("TOKEN");
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it("still dispatches STOP_LOADING when the request fails", async () => {
+ getRequest.mockImplementation(
+ () => () => () => Promise.reject(new Error("API error"))
+ );
+
+ const store = mockStore({
+ currentSummitState: { currentSummit: { id: 42 } },
+ currentSponsorState: { entity: { id: 7 } }
+ });
+
+ await store.dispatch(getSponsorFormManagedItem(9, 100)).catch(() => {});
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("STOP_LOADING");
+ });
+ });
});
diff --git a/src/actions/form-template-item-actions.js b/src/actions/form-template-item-actions.js
index fe6d3510b..261e7be0b 100644
--- a/src/actions/form-template-item-actions.js
+++ b/src/actions/form-template-item-actions.js
@@ -137,7 +137,7 @@ export const getFormTemplateItem =
createAction(RECEIVE_FORM_TEMPLATE_ITEM),
`${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${formTemplateId}/items/${formTemplateItemId}`,
snackbarErrorHandler
- )(params)(dispatch).then(() => {
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -387,7 +387,8 @@ export const deleteItemImage = (
) => {
const settings = {
url: `${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${formTemplateId}/items/${formTemplateItemId}/images`,
- deletedActionName: FORM_TEMPLATE_ITEM_IMAGE_DELETED
+ deletedActionName: FORM_TEMPLATE_ITEM_IMAGE_DELETED,
+ payload: { formTemplateItemId }
};
return deleteFile(imageId, settings);
};
diff --git a/src/actions/inventory-item-actions.js b/src/actions/inventory-item-actions.js
index 99dae7462..d47e9beae 100644
--- a/src/actions/inventory-item-actions.js
+++ b/src/actions/inventory-item-actions.js
@@ -159,7 +159,7 @@ export const getInventoryItem = (inventoryItemId) => async (dispatch) => {
createAction(RECEIVE_INVENTORY_ITEM),
`${window.INVENTORY_API_BASE_URL}/api/v1/inventory-items/${inventoryItemId}`,
snackbarErrorHandler
- )(params)(dispatch).then(() => {
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -351,7 +351,8 @@ const saveItemImages = (inventoryItem) => {
export const deleteInventoryItemImage = (inventoryItemId, imageId) => {
const settings = {
url: `${window.INVENTORY_API_BASE_URL}/api/v1/inventory-items/${inventoryItemId}/images`,
- deletedActionName: INVENTORY_ITEM_IMAGE_DELETED
+ deletedActionName: INVENTORY_ITEM_IMAGE_DELETED,
+ payload: { inventoryItemId }
};
return deleteFile(imageId, settings);
};
diff --git a/src/actions/inventory-shared-actions.js b/src/actions/inventory-shared-actions.js
index 69da56874..d959fb35c 100644
--- a/src/actions/inventory-shared-actions.js
+++ b/src/actions/inventory-shared-actions.js
@@ -234,7 +234,11 @@ export const saveFiles =
if (file.id) {
return putRequest(
null,
- createAction(settings.updatedActionName),
+ ({ response }) =>
+ createAction(settings.updatedActionName)({
+ response,
+ ...settings.payload
+ }),
`${settings.url}${file.id}/`,
file,
snackbarErrorHandler,
@@ -243,7 +247,11 @@ export const saveFiles =
}
return postRequest(
null,
- createAction(settings.addedActionName),
+ ({ response }) =>
+ createAction(settings.addedActionName)({
+ response,
+ ...settings.payload
+ }),
settings.url,
file,
snackbarErrorHandler,
@@ -261,7 +269,7 @@ export const deleteFile =
if (!settingsValidation.isValid) {
console.error(settingsValidation.error);
- return;
+ return false;
}
const accessToken = await getAccessTokenSafely();
@@ -273,13 +281,16 @@ 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)
+ .then(() => true)
+ .catch(() => false)
+ .finally(() => {
+ dispatch(stopLoading());
+ });
};
/* ************************************ ARCHIVE ************************************ */
diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js
index 5492628e7..f524f2c6a 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 { saveFiles, deleteFile } from "./inventory-shared-actions";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
@@ -97,7 +98,10 @@ 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";
+export const SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED =
+ "SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED";
// ITEMS
export const REQUEST_SPONSOR_FORM_ITEMS = "REQUEST_SPONSOR_FORM_ITEMS";
export const RECEIVE_SPONSOR_FORM_ITEMS = "RECEIVE_SPONSOR_FORM_ITEMS";
@@ -105,8 +109,8 @@ 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_ITEM_IMAGE_ADDED = "SPONSOR_FORM_ITEM_IMAGE_ADDED";
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 +853,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}`);
@@ -1198,7 +1203,7 @@ export const getSponsorFormItem =
createAction(RECEIVE_SPONSOR_FORM_ITEM),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${itemId}`,
authErrorHandler
- )(params)(dispatch).then(() => {
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -1231,35 +1236,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 =
@@ -1271,7 +1260,8 @@ export const saveSponsorFormItem =
dispatch(startLoading());
const params = {
- access_token: accessToken
+ access_token: accessToken,
+ expand: "images"
};
const normalizedEntity = normalizeItem(entity);
@@ -1283,28 +1273,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());
});
@@ -1319,7 +1301,8 @@ export const updateSponsorFormItem =
dispatch(startLoading());
const params = {
- access_token: accessToken
+ access_token: accessToken,
+ expand: "images"
};
const normalizedEntity = normalizeItem(entity);
@@ -1331,28 +1314,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 +1414,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 +1437,26 @@ const normalizeItem = (entity) => {
return normalizedEntity;
};
+const saveNewItemImages =
+ (formId, formItemId, images = []) =>
+ async (dispatch, getState) => {
+ const newImages = images.filter((img) => img.file_path);
+
+ if (newImages.length === 0) return Promise.resolve();
+
+ const { currentSummitState } = getState();
+ const { currentSummit } = currentSummitState;
+
+ const settings = {
+ url: `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/show-forms/${formId}/items/${formItemId}/images`,
+ addedActionName: SPONSOR_FORM_ITEM_IMAGE_ADDED,
+ updatedActionName: SPONSOR_FORM_ITEM_IMAGE_ADDED,
+ payload: { itemId: formItemId }
+ };
+
+ return saveFiles(newImages, settings)(dispatch);
+ };
+
export const addInventoryItems =
(formId, itemIds) => async (dispatch, getState) => {
const { currentSummitState } = getState();
@@ -1509,6 +1506,7 @@ export const saveSponsorFormManagedItem =
dispatch(startLoading());
const params = {
+ expand: "images",
access_token: accessToken
};
@@ -1523,16 +1521,22 @@ export const saveSponsorFormManagedItem =
snackbarErrorHandler,
entity
)(params)(dispatch)
- .then(() => {
- dispatch(
- snackbarSuccessHandler({
- title: T.translate("general.success"),
- html: T.translate(
- "edit_sponsor.forms_tab.form_manage_items.item_updated"
- )
- })
- );
- })
+ .then(() =>
+ saveNewManagedItemImages(
+ formId,
+ entity.id,
+ entity.images
+ )(dispatch, getState).then(() => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate(
+ "edit_sponsor.forms_tab.form_manage_items.item_updated"
+ )
+ })
+ );
+ })
+ )
.finally(() => {
dispatch(stopLoading());
});
@@ -1554,9 +1558,15 @@ export const saveSponsorFormManagedItem =
snackbarErrorHandler,
entity
)(params)(dispatch)
- .then(() => {
- dispatch(snackbarSuccessHandler(successMessage));
- })
+ .then(({ response }) =>
+ saveNewManagedItemImages(
+ formId,
+ response.id,
+ entity.images
+ )(dispatch, getState).then(() => {
+ dispatch(snackbarSuccessHandler(successMessage));
+ })
+ )
.finally(() => {
dispatch(stopLoading());
});
@@ -1571,13 +1581,39 @@ const normalizeManagedItem = (entity) => {
normalizedEntity.meta_fields = normalizedEntity.meta_fields?.filter(
(mf) => mf.name
);
- normalizedEntity.images = normalizedEntity.images?.filter(
- (img) => img.file_path
- );
+
+ // Images are never round-tripped inline here either - see normalizeItem's
+ // comment on the sibling (non-customized) save path. New uploads are
+ // persisted separately via saveNewManagedItemImages once the item itself
+ // is saved.
+ delete normalizedEntity.images;
return normalizedEntity;
};
+const saveNewManagedItemImages =
+ (formId, formItemId, images = []) =>
+ async (dispatch, getState) => {
+ const newImages = images.filter((img) => img.file_path);
+
+ if (newImages.length === 0) return Promise.resolve();
+
+ 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`,
+ addedActionName: SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED,
+ updatedActionName: SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED,
+ payload: { itemId: formItemId }
+ };
+
+ return saveFiles(newImages, settings)(dispatch);
+ };
+
export const deleteSponsorFormManagedItem =
(formId, itemId) => async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
@@ -1634,7 +1670,7 @@ export const getSponsorFormManagedItem =
createAction(RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${itemId}`,
authErrorHandler
- )(params)(dispatch).then(() => {
+ )(params)(dispatch).finally(() => {
dispatch(stopLoading());
});
};
@@ -1752,3 +1788,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/__tests__/form-template-item-list-page.test.js b/src/pages/sponsors-global/form-templates/__tests__/form-template-item-list-page.test.js
index 36b926b73..ff042d08b 100644
--- a/src/pages/sponsors-global/form-templates/__tests__/form-template-item-list-page.test.js
+++ b/src/pages/sponsors-global/form-templates/__tests__/form-template-item-list-page.test.js
@@ -1,14 +1,22 @@
import React from "react";
-import { waitFor } from "@testing-library/react";
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import flushPromises from "flush-promises";
import FormTemplateItemListPage from "../form-template-item-list-page";
import { renderWithRedux } from "../../../../utils/test-utils";
-import { getFormTemplateItems } from "../../../../actions/form-template-item-actions";
+import {
+ getFormTemplateItems,
+ getFormTemplateItem,
+ deleteItemImage
+} from "../../../../actions/form-template-item-actions";
import { getFormTemplate } from "../../../../actions/form-template-actions";
import { DEFAULT_CURRENT_PAGE } from "../../../../utils/constants";
jest.mock("../../../../actions/form-template-item-actions", () => ({
...jest.requireActual("../../../../actions/form-template-item-actions"),
- getFormTemplateItems: jest.fn(() => () => Promise.resolve())
+ getFormTemplateItems: jest.fn(() => () => Promise.resolve()),
+ getFormTemplateItem: jest.fn(() => () => Promise.resolve()),
+ deleteItemImage: jest.fn(() => () => Promise.resolve(true))
}));
jest.mock("../../../../actions/form-template-actions", () => ({
@@ -16,6 +24,31 @@ jest.mock("../../../../actions/form-template-actions", () => ({
getFormTemplate: jest.fn(() => () => Promise.resolve())
}));
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ data, onEdit }) => (
+
+ {data.map((row) => (
+
+ ))}
+
+ )
+}));
+
+jest.mock(
+ "../sponsor-inventory-popup",
+ () =>
+ function MockSponsorInventoryDialog({ onImageDeleted }) {
+ return (
+
+ );
+ }
+);
+
describe("FormTemplateItemListPage", () => {
const formTemplateId = 123;
const initialPage = 2;
@@ -23,9 +56,12 @@ describe("FormTemplateItemListPage", () => {
const order = "name";
const orderDir = 1;
const showArchived = false;
- const buildInitialState = () => ({
+ const buildInitialState = ({
+ formTemplateItems = [],
+ currentFormTemplateItem = {}
+ } = {}) => ({
currentFormTemplateItemListState: {
- formTemplateItems: [],
+ formTemplateItems,
term: "",
order,
orderDir,
@@ -40,7 +76,7 @@ describe("FormTemplateItemListPage", () => {
errors: {}
},
currentFormTemplateItemState: {
- entity: {},
+ entity: currentFormTemplateItem,
errors: {}
}
});
@@ -74,4 +110,64 @@ describe("FormTemplateItemListPage", () => {
});
});
});
+
+ describe("image removal guard", () => {
+ const openItemDialog = async () => {
+ const user = userEvent.setup();
+ await user.click(screen.getByText("edit-row-1"));
+ await waitFor(() => expect(getFormTemplateItem).toHaveBeenCalled());
+ await user.click(screen.getByText("mock-remove-item-image"));
+ };
+
+ test.each([
+ ["an unsaved entity (no id)", {}, null],
+ ["a persisted item, delete succeeds", { id: 55 }, true],
+ ["a persisted item, delete fails", { id: 55 }, false]
+ ])(
+ "removing an image for %s",
+ async (_label, currentFormTemplateItem, deleteSucceeds) => {
+ if (deleteSucceeds !== null) {
+ deleteItemImage.mockImplementation(
+ () => () => Promise.resolve(deleteSucceeds)
+ );
+ }
+
+ renderWithRedux(
+ ,
+ {
+ initialState: buildInitialState({
+ formTemplateItems: [{ id: 1, code: "A", name: "Item A" }],
+ currentFormTemplateItem
+ })
+ }
+ );
+
+ await openItemDialog();
+
+ if (deleteSucceeds === null) {
+ expect(deleteItemImage).not.toHaveBeenCalled();
+ return;
+ }
+
+ expect(deleteItemImage).toHaveBeenCalledWith(
+ formTemplateId,
+ currentFormTemplateItem.id,
+ 999
+ );
+
+ if (deleteSucceeds) {
+ await flushPromises();
+ expect(getFormTemplateItem).toHaveBeenCalledTimes(1);
+ } else {
+ await waitFor(() =>
+ expect(getFormTemplateItem).toHaveBeenCalledTimes(2)
+ );
+ expect(getFormTemplateItem).toHaveBeenLastCalledWith(
+ formTemplateId,
+ currentFormTemplateItem.id
+ );
+ }
+ }
+ );
+ });
});
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..69e339a6e 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,14 @@ const FormTemplateItemListPage = ({
).catch(() => {})
);
+ const handleRemoveImage = (imageId) => {
+ if (!currentFormTemplateItem?.id) return;
+ const itemId = currentFormTemplateItem.id;
+ deleteItemImage(formTemplateId, itemId, imageId).then((success) => {
+ if (!success) getFormTemplateItem(formTemplateId, itemId).catch(() => {});
+ });
+ };
+
const columns = [
{
columnKey: "code",
@@ -319,7 +327,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/__tests__/inventory-list-page.test.js b/src/pages/sponsors-global/inventory/__tests__/inventory-list-page.test.js
new file mode 100644
index 000000000..51bdb54a6
--- /dev/null
+++ b/src/pages/sponsors-global/inventory/__tests__/inventory-list-page.test.js
@@ -0,0 +1,120 @@
+import React from "react";
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import flushPromises from "flush-promises";
+import InventoryListPage from "../inventory-list-page";
+import { renderWithRedux } from "../../../../utils/test-utils";
+import {
+ getInventoryItem,
+ deleteInventoryItemImage
+} from "../../../../actions/inventory-item-actions";
+
+jest.mock("../../../../actions/inventory-item-actions", () => ({
+ ...jest.requireActual("../../../../actions/inventory-item-actions"),
+ getInventoryItems: jest.fn(() => () => Promise.resolve()),
+ getInventoryItem: jest.fn(() => () => Promise.resolve()),
+ deleteInventoryItemImage: jest.fn(() => () => Promise.resolve(true))
+}));
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ data, onEdit }) => (
+
+ {data.map((row) => (
+
+ ))}
+
+ )
+}));
+
+jest.mock(
+ "../../form-templates/sponsor-inventory-popup",
+ () =>
+ function MockSponsorInventoryDialog({ onImageDeleted }) {
+ return (
+
+ );
+ }
+);
+
+const buildInitialState = ({
+ inventoryItems = [],
+ currentInventoryItem = {}
+} = {}) => ({
+ currentInventoryItemListState: {
+ inventoryItems,
+ term: "",
+ order: "name",
+ orderDir: 1,
+ currentPage: 1,
+ lastPage: 1,
+ perPage: 10,
+ totalInventoryItems: inventoryItems.length,
+ showArchived: false
+ },
+ currentInventoryItemState: {
+ entity: currentInventoryItem,
+ errors: {}
+ }
+});
+
+describe("InventoryListPage image removal guard", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const openItemDialog = async () => {
+ const user = userEvent.setup();
+ await user.click(screen.getByText("edit-row-1"));
+ await waitFor(() => expect(getInventoryItem).toHaveBeenCalled());
+ await user.click(screen.getByText("mock-remove-item-image"));
+ };
+
+ test.each([
+ ["an unsaved entity (no id)", {}, null],
+ ["a persisted item, delete succeeds", { id: 77 }, true],
+ ["a persisted item, delete fails", { id: 77 }, false]
+ ])(
+ "removing an image for %s",
+ async (_label, currentInventoryItem, deleteSucceeds) => {
+ if (deleteSucceeds !== null) {
+ deleteInventoryItemImage.mockImplementation(
+ () => () => Promise.resolve(deleteSucceeds)
+ );
+ }
+
+ renderWithRedux(, {
+ initialState: buildInitialState({
+ inventoryItems: [{ id: 1, code: "A", name: "Item A" }],
+ currentInventoryItem
+ })
+ });
+
+ await openItemDialog();
+
+ if (deleteSucceeds === null) {
+ expect(deleteInventoryItemImage).not.toHaveBeenCalled();
+ return;
+ }
+
+ expect(deleteInventoryItemImage).toHaveBeenCalledWith(
+ currentInventoryItem.id,
+ 999
+ );
+
+ if (deleteSucceeds) {
+ await flushPromises();
+ expect(getInventoryItem).toHaveBeenCalledTimes(1);
+ } else {
+ await waitFor(() => expect(getInventoryItem).toHaveBeenCalledTimes(2));
+ expect(getInventoryItem).toHaveBeenLastCalledWith(
+ currentInventoryItem.id
+ );
+ }
+ }
+ );
+});
diff --git a/src/pages/sponsors-global/inventory/inventory-list-page.js b/src/pages/sponsors-global/inventory/inventory-list-page.js
index a3ba172f5..2919aabc1 100644
--- a/src/pages/sponsors-global/inventory/inventory-list-page.js
+++ b/src/pages/sponsors-global/inventory/inventory-list-page.js
@@ -149,6 +149,14 @@ const InventoryListPage = ({
? unarchiveInventoryItem(item)
: archiveInventoryItem(item);
+ const handleRemoveImage = (imageId) => {
+ if (!currentInventoryItem?.id) return;
+ const itemId = currentInventoryItem.id;
+ deleteInventoryItemImage(itemId, imageId).then((success) => {
+ if (!success) getInventoryItem(itemId).catch(() => {});
+ });
+ };
+
const columns = [
{
columnKey: "code",
@@ -291,7 +299,7 @@ const InventoryListPage = ({
onClose={handleClose}
onMetaFieldTypeDeleted={deleteInventoryItemMetaFieldType}
onMetaFieldTypeValueDeleted={deleteInventoryItemMetaFieldTypeValue}
- onImageDeleted={deleteInventoryItemImage}
+ onImageDeleted={handleRemoveImage}
/>
)}
@@ -312,7 +320,6 @@ export default connect(mapStateToProps, {
getInventoryItem,
resetInventoryItemForm,
saveInventoryItem,
-
deleteInventoryItemImage,
deleteInventoryItemMetaFieldType,
deleteInventoryItemMetaFieldTypeValue,
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/__tests__/sponsor-form-item-list-page.test.js b/src/pages/sponsors/sponsor-form-item-list-page/__tests__/sponsor-form-item-list-page.test.js
index 73f7a77c7..0916fc17c 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/__tests__/sponsor-form-item-list-page.test.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/__tests__/sponsor-form-item-list-page.test.js
@@ -1,14 +1,17 @@
import React from "react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
+import flushPromises from "flush-promises";
import SponsorFormItemListPage from "../index";
import { renderWithRedux } from "../../../../utils/test-utils";
jest.mock("../../../../actions/sponsor-forms-actions", () => ({
...jest.requireActual("../../../../actions/sponsor-forms-actions"),
getSponsorFormItems: jest.fn(() => () => Promise.resolve()),
+ getSponsorFormItem: jest.fn(() => () => Promise.resolve()),
updateSponsorFormItem: jest.fn(() => () => Promise.resolve()),
- addInventoryItems: jest.fn(() => () => Promise.resolve())
+ addInventoryItems: jest.fn(() => () => Promise.resolve()),
+ removeItemFile: jest.fn(() => () => Promise.resolve(true))
}));
jest.mock("../../../../actions/inventory-item-actions", () => ({
@@ -26,10 +29,24 @@ jest.mock(
}
);
+jest.mock(
+ "../components/sponsor-form-item-popup",
+ () =>
+ function MockSponsorFormItemPopup({ onRemoveImage }) {
+ return (
+
+ );
+ }
+);
+
const {
getSponsorFormItems,
+ getSponsorFormItem,
updateSponsorFormItem,
- addInventoryItems
+ addInventoryItems,
+ removeItemFile
} = require("../../../../actions/sponsor-forms-actions");
const buildItem = (id) => ({
@@ -44,7 +61,7 @@ const buildItem = (id) => ({
images: []
});
-const renderPage = () =>
+const renderPage = (currentItem = {}) =>
renderWithRedux(
initialState: {
sponsorFormItemsListState: {
items: [buildItem(1)],
- currentItem: {},
+ currentItem,
currentPage: 3,
perPage: 5,
order: "code",
@@ -122,3 +139,53 @@ describe("SponsorFormItemListPage inline cell edit", () => {
);
});
});
+
+describe("SponsorFormItemListPage image removal guard", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const openItemPopup = async () => {
+ const user = userEvent.setup();
+ await user.click(screen.getByText("sponsor_form_item_list.add_item"));
+ await user.click(screen.getByText("mock-remove-item-image"));
+ };
+
+ it.each([
+ ["an unsaved entity (no id)", {}, null],
+ ["a persisted item, delete succeeds", { id: 42 }, true],
+ ["a persisted item, delete fails", { id: 42 }, false]
+ ])(
+ "removing an image for %s",
+ async (_label, currentItem, deleteSucceeds) => {
+ if (deleteSucceeds !== null) {
+ removeItemFile.mockImplementation(
+ () => () => Promise.resolve(deleteSucceeds)
+ );
+ }
+
+ renderPage(currentItem);
+
+ await openItemPopup();
+
+ if (deleteSucceeds === null) {
+ expect(removeItemFile).not.toHaveBeenCalled();
+ return;
+ }
+
+ expect(removeItemFile).toHaveBeenCalledWith("FORM1", currentItem.id, 999);
+
+ if (deleteSucceeds) {
+ await flushPromises();
+ expect(getSponsorFormItem).not.toHaveBeenCalled();
+ } else {
+ await waitFor(() =>
+ expect(getSponsorFormItem).toHaveBeenCalledWith(
+ "FORM1",
+ currentItem.id
+ )
+ );
+ }
+ }
+ );
+});
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..87721eef9 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,13 @@ const SponsorFormItemListPage = ({
);
};
+ const handleRemoveItemImage = (imageId) => {
+ if (!currentItem?.id) return;
+ removeItemFile(formId, currentItem.id, imageId).then((success) => {
+ if (!success) getSponsorFormItem(formId, currentItem.id).catch(() => {});
+ });
+ };
+
const handleAddFromInventory = (itemIds) =>
addInventoryItems(formId, itemIds).then(() =>
getSponsorFormItems(
@@ -359,6 +368,7 @@ const SponsorFormItemListPage = ({
item={currentItem}
onSave={handleSaveItem}
onClose={handleClosePopup}
+ onRemoveImage={handleRemoveItemImage}
/>
)}
{openPopup === "inventory" && (
@@ -391,5 +401,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/__tests__/sponsor-forms-manage-items.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/__tests__/sponsor-forms-manage-items.test.js
new file mode 100644
index 000000000..b0ebaf4e1
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/__tests__/sponsor-forms-manage-items.test.js
@@ -0,0 +1,133 @@
+import React from "react";
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import flushPromises from "flush-promises";
+import SponsorFormsManageItems from "../sponsor-forms-manage-items";
+import { renderWithRedux } from "../../../../../../../../utils/test-utils";
+import {
+ getSponsorFormManagedItem,
+ removeSponsorCustomizedFormItemImages
+} from "../../../../../../../../actions/sponsor-forms-actions";
+
+jest.mock("../../../../../../../../actions/sponsor-forms-actions", () => ({
+ ...jest.requireActual(
+ "../../../../../../../../actions/sponsor-forms-actions"
+ ),
+ getSponsorCustomizedFormItems: jest.fn(() => () => Promise.resolve()),
+ getSponsorFormManagedItem: jest.fn(() => () => Promise.resolve()),
+ removeSponsorCustomizedFormItemImages: jest.fn(
+ () => () => Promise.resolve(true)
+ )
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/editable-table",
+ () => ({
+ __esModule: true,
+ default: ({ data, onEdit }) => (
+
+ {data.map((row) => (
+
+ ))}
+
+ )
+ })
+);
+
+jest.mock(
+ "../../../../../../../sponsors-global/form-templates/sponsor-inventory-popup",
+ () =>
+ function MockSponsorInventoryDialog({ onImageDeleted }) {
+ return (
+
+ );
+ }
+);
+
+const buildInitialState = ({ items = [], currentItem = {} } = {}) => ({
+ sponsorCustomizedFormItemsListState: {
+ items,
+ showArchived: false,
+ term: "",
+ order: "name",
+ orderDir: 1,
+ currentPage: 1,
+ lastPage: 1,
+ perPage: 10,
+ totalCount: items.length,
+ currentItem
+ },
+ loggedUserState: {
+ member: {
+ groups: [{ id: 1, code: "super-admins" }]
+ }
+ }
+});
+
+describe("SponsorFormsManageItems image removal guard", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const openItemDialog = async () => {
+ const user = userEvent.setup();
+ await user.click(screen.getByText("edit-row-1"));
+ await waitFor(() => expect(getSponsorFormManagedItem).toHaveBeenCalled());
+ await user.click(screen.getByText("mock-remove-item-image"));
+ };
+
+ test.each([
+ ["an unsaved entity (no id)", {}, null],
+ ["a persisted item, delete succeeds", { id: 33 }, true],
+ ["a persisted item, delete fails", { id: 33 }, false]
+ ])(
+ "removing an image for %s",
+ async (_label, currentItem, deleteSucceeds) => {
+ if (deleteSucceeds !== null) {
+ removeSponsorCustomizedFormItemImages.mockImplementation(
+ () => () => Promise.resolve(deleteSucceeds)
+ );
+ }
+
+ renderWithRedux(
+ ,
+ {
+ initialState: buildInitialState({
+ items: [{ id: 1, code: "A", name: "Item A" }],
+ currentItem
+ })
+ }
+ );
+
+ await openItemDialog();
+
+ if (deleteSucceeds === null) {
+ expect(removeSponsorCustomizedFormItemImages).not.toHaveBeenCalled();
+ return;
+ }
+
+ expect(removeSponsorCustomizedFormItemImages).toHaveBeenCalledWith(
+ "FORM1",
+ currentItem.id,
+ 999
+ );
+
+ if (deleteSucceeds) {
+ await flushPromises();
+ expect(getSponsorFormManagedItem).toHaveBeenCalledTimes(1);
+ } else {
+ await waitFor(() =>
+ expect(getSponsorFormManagedItem).toHaveBeenCalledTimes(2)
+ );
+ expect(getSponsorFormManagedItem).toHaveBeenLastCalledWith(
+ "FORM1",
+ currentItem.id
+ );
+ }
+ }
+ );
+});
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..cff1c1065 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,16 @@ const SponsorFormsManageItems = ({
);
};
+ const handleImageRemove = (imageId) => {
+ if (!currentInventoryItem?.id) return;
+ const itemId = currentInventoryItem.id;
+ removeSponsorCustomizedFormItemImages(formId, itemId, imageId).then(
+ (success) => {
+ if (!success) getSponsorFormManagedItem(formId, itemId).catch(() => {});
+ }
+ );
+ };
+
const sponsorItemColumns = [
{
columnKey: "code",
@@ -382,6 +394,7 @@ const SponsorFormsManageItems = ({
entity={currentInventoryItem}
onSave={handleItemSave}
onClose={handleClose}
+ onImageDeleted={handleImageRemove}
/>
)}
@@ -409,7 +422,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..4ce1d24bd 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,8 @@
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_IMAGE_ADDED,
SPONSOR_FORM_MANAGED_ITEM_UPDATED
} from "../../../actions/sponsor-forms-actions";
@@ -46,7 +48,7 @@ const buildItem = (overrides = {}) => ({
describe("sponsorCustomizedFormItemsListReducer", () => {
describe("RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM", () => {
- it("maps file_url to file_path on each image — the edit-form image fix", () => {
+ it("stores images as received from the API, without a file_path mapping", () => {
const result = sponsorCustomizedFormItemsListReducer(DEFAULT_STATE, {
type: RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM,
payload: {
@@ -62,13 +64,11 @@ describe("sponsorCustomizedFormItemsListReducer", () => {
expect(result.currentItem.images).toEqual([
{
id: 10,
- file_url: "https://cdn/a.png",
- file_path: "https://cdn/a.png"
+ file_url: "https://cdn/a.png"
},
{
id: 11,
- file_url: "https://cdn/b.png",
- file_path: "https://cdn/b.png"
+ file_url: "https://cdn/b.png"
}
]);
});
@@ -92,6 +92,140 @@ 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_IMAGE_ADDED", () => {
+ it.each([
+ [
+ "with existing images",
+ [{ id: 10 }],
+ { id: 11 },
+ [{ id: 10 }, { id: 11 }]
+ ],
+ ["with no images (undefined)", undefined, { id: 10 }, [{ id: 10 }]]
+ ])(
+ "appends the new image to currentItem and its matching list item (%s)",
+ (_label, initialImages, newImage, expectedImages) => {
+ const state = {
+ ...DEFAULT_STATE,
+ currentItem: {
+ ...DEFAULT_STATE.currentItem,
+ id: 1,
+ images: initialImages
+ },
+ items: [
+ buildItem({ id: 1, images: initialImages }),
+ buildItem({ id: 2, images: [{ id: 12 }] })
+ ]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED,
+ payload: { response: newImage, itemId: 1 }
+ });
+
+ expect(result.currentItem.images).toEqual(expectedImages);
+ expect(result.items[0].images).toEqual(expectedImages);
+ expect(result.items[1].images).toEqual([{ id: 12 }]);
+ }
+ );
+
+ it("leaves currentItem untouched when the new image belongs to a different item", () => {
+ // This is exactly the case that broke before this action got its own
+ // type: an image upload response ({id, file_url}) used to be dispatched
+ // as SPONSOR_FORM_MANAGED_ITEM_UPDATED, which that reducer case reads
+ // as an item - matching state.items by the image's id and clobbering
+ // whichever unrelated item happened to share that numeric id.
+ const state = {
+ ...DEFAULT_STATE,
+ currentItem: {
+ ...DEFAULT_STATE.currentItem,
+ id: 2,
+ images: [{ id: 12 }]
+ },
+ items: [
+ buildItem({ id: 1, images: [{ id: 10 }] }),
+ buildItem({ id: 2, images: [{ id: 12 }] })
+ ]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED,
+ payload: { response: { id: 11 }, itemId: 1 }
+ });
+
+ expect(result.currentItem).toEqual(state.currentItem);
+ expect(result.items[0].images).toEqual([{ id: 10 }, { 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" }];
@@ -121,5 +255,31 @@ describe("sponsorCustomizedFormItemsListReducer", () => {
expect(result.items[0].images).toBe(images);
expect(result.items[1].name).toBe("Other");
});
+
+ it("preserves the existing item's images when the response omits them", () => {
+ // Regression for the handleCellEdit path (sponsor-forms-manage-items.js)
+ // which saves via this action with no follow-up refetch - a response
+ // that omits images must not clobber the row's thumbnail.
+ const images = [{ id: 20, file_url: "https://cdn/img.png" }];
+ const state = {
+ ...DEFAULT_STATE,
+ items: [buildItem({ id: 1, name: "Before", images })]
+ };
+
+ const result = sponsorCustomizedFormItemsListReducer(state, {
+ type: SPONSOR_FORM_MANAGED_ITEM_UPDATED,
+ payload: {
+ response: buildItem({
+ id: 1,
+ name: "After",
+ early_bird_rate: 500,
+ images: undefined
+ })
+ }
+ });
+
+ expect(result.items[0].name).toBe("After");
+ expect(result.items[0].images).toBe(images);
+ });
});
});
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..501f37b4c 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,7 +8,10 @@ import {
RESET_SPONSOR_FORM_ITEM,
SPONSOR_FORM_ITEM_ARCHIVED,
SPONSOR_FORM_ITEM_DELETED,
- SPONSOR_FORM_ITEM_UNARCHIVED
+ SPONSOR_FORM_ITEM_FILE_DELETED,
+ SPONSOR_FORM_ITEM_IMAGE_ADDED,
+ SPONSOR_FORM_ITEM_UNARCHIVED,
+ SPONSOR_FORM_ITEM_UPDATED
} from "../../../actions/sponsor-forms-actions";
function createDefaultState() {
@@ -189,6 +192,55 @@ describe("SponsorFormItemsListReducer", () => {
}
});
});
+
+ it("keeps images in their API shape (id, file_url) without a file_path mapping", () => {
+ 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" },
+ { id: 11, file_url: "https://cdn/b.png" }
+ ]);
+ });
+
+ it("defaults images to [] when the response omits the field", () => {
+ const item = {
+ id: "A",
+ code: "A",
+ name: "A",
+ early_bird_rate: 100,
+ standard_rate: 100,
+ onsite_rate: 100,
+ default_quantity: "100",
+ is_archived: true,
+ meta_fields: []
+ };
+
+ result = SponsorFormItemsListReducer(initialState, {
+ type: RECEIVE_SPONSOR_FORM_ITEM,
+ payload: { response: item }
+ });
+
+ expect(result.currentItem.images).toEqual([]);
+ });
});
describe("RESET_SPONSOR_FORM_ITEM", () => {
@@ -268,6 +320,263 @@ 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" }] }
+ ]);
+ });
+
+ it("defaults the matching list item's images to [] when it has none", () => {
+ const state = {
+ ...initialState,
+ currentItem: {
+ ...initialState.currentItem,
+ id: "A",
+ images: undefined
+ },
+ items: [
+ { id: "A", images: undefined },
+ { 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([]);
+ expect(result.items[0].images).toStrictEqual([]);
+ });
+ });
+
+ describe("SPONSOR_FORM_ITEM_IMAGE_ADDED", () => {
+ it.each([
+ [
+ "with existing images",
+ [{ id: "IMG_1" }],
+ { id: "IMG_2" },
+ [{ id: "IMG_1" }, { id: "IMG_2" }]
+ ],
+ [
+ "with no images (undefined)",
+ undefined,
+ { id: "IMG_1" },
+ [{ id: "IMG_1" }]
+ ]
+ ])(
+ "appends the new image to currentItem and its matching list item (%s)",
+ (_label, initialImages, newImage, expectedImages) => {
+ const state = {
+ ...initialState,
+ currentItem: {
+ ...initialState.currentItem,
+ id: "A",
+ images: initialImages
+ },
+ items: [
+ { id: "A", images: initialImages },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]
+ };
+
+ result = SponsorFormItemsListReducer(state, {
+ type: SPONSOR_FORM_ITEM_IMAGE_ADDED,
+ payload: { response: newImage, itemId: "A" }
+ });
+
+ expect(result.currentItem.images).toStrictEqual(expectedImages);
+ expect(result.items).toStrictEqual([
+ { id: "A", images: expectedImages },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]);
+ }
+ );
+
+ it("leaves currentItem untouched when the new image 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_IMAGE_ADDED,
+ payload: { response: { id: "IMG_2" }, itemId: "A" }
+ });
+
+ expect(result.currentItem).toStrictEqual(state.currentItem);
+ expect(result.items).toStrictEqual([
+ { id: "A", images: [{ id: "IMG_1" }, { id: "IMG_2" }] },
+ { id: "B", images: [{ id: "IMG_3" }] }
+ ]);
+ });
+ });
+
+ describe("SPONSOR_FORM_ITEM_UPDATED", () => {
+ it("replaces the matching list item's fields from the response", () => {
+ const state = {
+ ...initialState,
+ items: [
+ {
+ id: "A",
+ code: "A",
+ name: "Before",
+ early_bird_rate: "$1.00",
+ standard_rate: "$1.00",
+ onsite_rate: "$1.00",
+ default_quantity: "100",
+ is_archived: false,
+ images: [{ id: "IMG_1" }]
+ },
+ {
+ id: "B",
+ code: "B",
+ name: "B",
+ early_bird_rate: "$1.00",
+ standard_rate: "$1.00",
+ onsite_rate: "$1.00",
+ default_quantity: "100",
+ is_archived: false,
+ images: []
+ }
+ ]
+ };
+
+ result = SponsorFormItemsListReducer(state, {
+ type: SPONSOR_FORM_ITEM_UPDATED,
+ payload: {
+ response: {
+ id: "A",
+ code: "A",
+ name: "After",
+ early_bird_rate: 500,
+ standard_rate: 600,
+ onsite_rate: 700,
+ default_quantity: "200",
+ is_archived: true
+ }
+ }
+ });
+
+ expect(result.items).toStrictEqual([
+ {
+ id: "A",
+ code: "A",
+ name: "After",
+ early_bird_rate: "$5.00",
+ standard_rate: "$6.00",
+ onsite_rate: "$7.00",
+ default_quantity: "200",
+ is_archived: true,
+ images: [{ id: "IMG_1" }]
+ },
+ {
+ id: "B",
+ code: "B",
+ name: "B",
+ early_bird_rate: "$1.00",
+ standard_rate: "$1.00",
+ onsite_rate: "$1.00",
+ default_quantity: "100",
+ is_archived: false,
+ images: []
+ }
+ ]);
+ });
+
+ it("leaves the list unchanged when the updated item isn't in the current page (e.g. a freshly created item)", () => {
+ const state = {
+ ...initialState,
+ items: [
+ {
+ id: "B",
+ code: "B",
+ name: "B",
+ early_bird_rate: "$1.00",
+ standard_rate: "$1.00",
+ onsite_rate: "$1.00",
+ default_quantity: "100",
+ is_archived: false,
+ images: []
+ }
+ ]
+ };
+
+ result = SponsorFormItemsListReducer(state, {
+ type: SPONSOR_FORM_ITEM_UPDATED,
+ payload: {
+ response: {
+ id: "NEW",
+ code: "NEW",
+ name: "New item",
+ early_bird_rate: 100,
+ standard_rate: 100,
+ onsite_rate: 100,
+ default_quantity: "1",
+ is_archived: false
+ }
+ }
+ });
+
+ expect(result.items).toStrictEqual(state.items);
+ });
+ });
+
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..1070469a7 100644
--- a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js
@@ -21,8 +21,10 @@ import {
SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED,
SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED,
SPONSOR_FORM_MANAGED_ITEM_UPDATED,
+ SPONSOR_FORM_MANAGED_ITEM_IMAGE_ADDED,
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";
@@ -111,14 +113,47 @@ const sponsorCustomizedFormItemsListReducer = (
const currentItem = {
...item,
- images: (item.images || []).map((img) => ({
- ...img,
- file_path: img.file_url
- })),
+ images: item.images ?? [],
meta_fields: (item.meta_fields ?? []).length > 0 ? item.meta_fields : []
};
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_FORM_MANAGED_ITEM_IMAGE_ADDED: {
+ const { response: image, itemId } = payload;
+ const currentItem =
+ state.currentItem.id === itemId
+ ? {
+ ...state.currentItem,
+ images: [...(state.currentItem.images ?? []), image]
+ }
+ : state.currentItem;
+
+ const items = state.items.map((item) =>
+ item.id === itemId
+ ? { ...item, images: [...(item.images ?? []), image] }
+ : item
+ );
+
+ return { ...state, currentItem, items };
+ }
case SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED: {
const { itemId } = payload;
const items = state.items.filter((it) => it.id !== itemId);
@@ -166,7 +201,7 @@ const sponsorCustomizedFormItemsListReducer = (
onsite_rate: formatRateFromCents(updatedItem.onsite_rate),
default_quantity: updatedItem.default_quantity,
is_archived: updatedItem.is_archived,
- images: updatedItem.images
+ images: updatedItem.images ?? item.images
}
: item
);
diff --git a/src/reducers/sponsors/sponsor-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
index 6c656794c..803c71898 100644
--- a/src/reducers/sponsors/sponsor-form-items-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-form-items-list-reducer.js
@@ -20,7 +20,10 @@ import {
RESET_SPONSOR_FORM_ITEM,
SPONSOR_FORM_ITEM_ARCHIVED,
SPONSOR_FORM_ITEM_DELETED,
- SPONSOR_FORM_ITEM_UNARCHIVED
+ SPONSOR_FORM_ITEM_FILE_DELETED,
+ SPONSOR_FORM_ITEM_IMAGE_ADDED,
+ SPONSOR_FORM_ITEM_UNARCHIVED,
+ SPONSOR_FORM_ITEM_UPDATED
} from "../../actions/sponsor-forms-actions";
import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions";
import { getSafePageAfterRemove } from "../../utils/methods";
@@ -99,12 +102,11 @@ 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 ?? [],
+ meta_fields: (item.meta_fields ?? []).length > 0 ? item.meta_fields : []
};
-
return { ...state, currentItem };
}
case RESET_SPONSOR_FORM_ITEM: {
@@ -116,6 +118,66 @@ 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_IMAGE_ADDED: {
+ const { response: image, itemId } = payload;
+ const currentItem =
+ state.currentItem.id === itemId
+ ? {
+ ...state.currentItem,
+ images: [...(state.currentItem.images ?? []), image]
+ }
+ : state.currentItem;
+
+ const items = state.items.map((item) =>
+ item.id === itemId
+ ? { ...item, images: [...(item.images ?? []), image] }
+ : item
+ );
+
+ return { ...state, currentItem, items };
+ }
+ case SPONSOR_FORM_ITEM_UPDATED: {
+ const updatedItem = payload.response;
+ const items = state.items.map((item) =>
+ item.id === updatedItem.id
+ ? {
+ id: updatedItem.id,
+ code: updatedItem.code,
+ name: updatedItem.name,
+ early_bird_rate: formatRateFromCents(updatedItem.early_bird_rate),
+ standard_rate: formatRateFromCents(updatedItem.standard_rate),
+ onsite_rate: formatRateFromCents(updatedItem.onsite_rate),
+ default_quantity: updatedItem.default_quantity,
+ is_archived: updatedItem.is_archived,
+ images: updatedItem.images ?? item.images
+ }
+ : item
+ );
+ return { ...state, items };
+ }
case SPONSOR_FORM_ITEM_ARCHIVED: {
const { id: itemId } = payload.response;
const { totalCount, perPage, currentPage } = state;
diff --git a/src/reducers/sponsors_inventory/__tests__/form-template-item-list-reducer.test.js b/src/reducers/sponsors_inventory/__tests__/form-template-item-list-reducer.test.js
new file mode 100644
index 000000000..528424976
--- /dev/null
+++ b/src/reducers/sponsors_inventory/__tests__/form-template-item-list-reducer.test.js
@@ -0,0 +1,38 @@
+import formTemplateItemListReducer from "../form-template-item-list-reducer";
+import { FORM_TEMPLATE_ITEM_IMAGE_DELETED } from "../../../actions/form-template-item-actions";
+
+describe("formTemplateItemListReducer", () => {
+ describe("FORM_TEMPLATE_ITEM_IMAGE_DELETED", () => {
+ it("removes the deleted image from the matching item's images in the list", () => {
+ const state = {
+ formTemplateItems: [
+ { id: 1, images: [{ id: 10 }] },
+ { id: 2, images: [{ id: 20 }] }
+ ]
+ };
+
+ const result = formTemplateItemListReducer(state, {
+ type: FORM_TEMPLATE_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10, formTemplateItemId: 1 }
+ });
+
+ expect(result.formTemplateItems).toEqual([
+ { id: 1, images: [] },
+ { id: 2, images: [{ id: 20 }] }
+ ]);
+ });
+
+ it("leaves other items untouched when the deleted image belongs to a different item", () => {
+ const state = {
+ formTemplateItems: [{ id: 1, images: [{ id: 10 }] }]
+ };
+
+ const result = formTemplateItemListReducer(state, {
+ type: FORM_TEMPLATE_ITEM_IMAGE_DELETED,
+ payload: { fileId: 999, formTemplateItemId: 2 }
+ });
+
+ expect(result.formTemplateItems).toEqual(state.formTemplateItems);
+ });
+ });
+});
diff --git a/src/reducers/sponsors_inventory/__tests__/inventory-item-list-reducer.test.js b/src/reducers/sponsors_inventory/__tests__/inventory-item-list-reducer.test.js
new file mode 100644
index 000000000..8bcc53606
--- /dev/null
+++ b/src/reducers/sponsors_inventory/__tests__/inventory-item-list-reducer.test.js
@@ -0,0 +1,38 @@
+import inventoryItemListReducer from "../inventory-item-list-reducer";
+import { INVENTORY_ITEM_IMAGE_DELETED } from "../../../actions/inventory-item-actions";
+
+describe("inventoryItemListReducer", () => {
+ describe("INVENTORY_ITEM_IMAGE_DELETED", () => {
+ it("removes the deleted image from the matching item's images in the list", () => {
+ const state = {
+ inventoryItems: [
+ { id: 1, images: [{ id: 10 }] },
+ { id: 2, images: [{ id: 20 }] }
+ ]
+ };
+
+ const result = inventoryItemListReducer(state, {
+ type: INVENTORY_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10, inventoryItemId: 1 }
+ });
+
+ expect(result.inventoryItems).toEqual([
+ { id: 1, images: [] },
+ { id: 2, images: [{ id: 20 }] }
+ ]);
+ });
+
+ it("leaves other items untouched when the deleted image belongs to a different item", () => {
+ const state = {
+ inventoryItems: [{ id: 1, images: [{ id: 10 }] }]
+ };
+
+ const result = inventoryItemListReducer(state, {
+ type: INVENTORY_ITEM_IMAGE_DELETED,
+ payload: { fileId: 999, inventoryItemId: 2 }
+ });
+
+ expect(result.inventoryItems).toEqual(state.inventoryItems);
+ });
+ });
+});
diff --git a/src/reducers/sponsors_inventory/__tests__/inventory-item-reducer.test.js b/src/reducers/sponsors_inventory/__tests__/inventory-item-reducer.test.js
new file mode 100644
index 000000000..7698e8d70
--- /dev/null
+++ b/src/reducers/sponsors_inventory/__tests__/inventory-item-reducer.test.js
@@ -0,0 +1,22 @@
+import inventoryItemReducer from "../inventory-item-reducer";
+import { INVENTORY_ITEM_IMAGE_DELETED } from "../../../actions/inventory-item-actions";
+
+describe("inventoryItemReducer", () => {
+ describe("INVENTORY_ITEM_IMAGE_DELETED", () => {
+ it("removes the deleted image by fileId from entity.images", () => {
+ const state = {
+ entity: {
+ id: 1,
+ images: [{ id: 10 }, { id: 11 }]
+ }
+ };
+
+ const result = inventoryItemReducer(state, {
+ type: INVENTORY_ITEM_IMAGE_DELETED,
+ payload: { fileId: 10 }
+ });
+
+ expect(result.entity.images).toEqual([{ id: 11 }]);
+ });
+ });
+});
diff --git a/src/reducers/sponsors_inventory/form-template-item-list-reducer.js b/src/reducers/sponsors_inventory/form-template-item-list-reducer.js
index d040b7a6b..81435b6d9 100644
--- a/src/reducers/sponsors_inventory/form-template-item-list-reducer.js
+++ b/src/reducers/sponsors_inventory/form-template-item-list-reducer.js
@@ -18,7 +18,8 @@ import {
FORM_TEMPLATE_ITEM_DELETED,
CHANGE_FORM_TEMPLATE_ITEM_SEARCH_TERM,
FORM_TEMPLATE_ITEM_ARCHIVED,
- FORM_TEMPLATE_ITEM_UNARCHIVED
+ FORM_TEMPLATE_ITEM_UNARCHIVED,
+ FORM_TEMPLATE_ITEM_IMAGE_DELETED
} from "../../actions/form-template-item-actions";
import { getSafePageAfterRemove } from "../../utils/methods";
@@ -143,6 +144,18 @@ const formTemplateItemListReducer = (state = DEFAULT_STATE, action = {}) => {
)
};
}
+ case FORM_TEMPLATE_ITEM_IMAGE_DELETED: {
+ const { fileId, formTemplateItemId } = payload;
+ const updatedFormTemplateItems = state.formTemplateItems.map((item) =>
+ item.id === formTemplateItemId
+ ? {
+ ...item,
+ images: item.images?.filter((img) => img.id !== fileId) ?? []
+ }
+ : item
+ );
+ return { ...state, formTemplateItems: updatedFormTemplateItems };
+ }
default:
return state;
}
diff --git a/src/reducers/sponsors_inventory/inventory-item-list-reducer.js b/src/reducers/sponsors_inventory/inventory-item-list-reducer.js
index 955ea04bb..dd93fae7a 100644
--- a/src/reducers/sponsors_inventory/inventory-item-list-reducer.js
+++ b/src/reducers/sponsors_inventory/inventory-item-list-reducer.js
@@ -23,7 +23,8 @@ import {
SET_SELECTED_ALL_INVENTORY_ITEMS,
INVENTORY_ITEM_ARCHIVED,
INVENTORY_ITEM_UNARCHIVED,
- INVENTORY_ITEM_IMAGE_SAVED
+ INVENTORY_ITEM_IMAGE_SAVED,
+ INVENTORY_ITEM_IMAGE_DELETED
} from "../../actions/inventory-item-actions";
import { getSafePageAfterRemove } from "../../utils/methods";
@@ -246,6 +247,18 @@ const inventoryItemListReducer = (state = DEFAULT_STATE, action = {}) => {
);
return { ...state, inventoryItems: updatedInventoryItems };
}
+ case INVENTORY_ITEM_IMAGE_DELETED: {
+ const { fileId, inventoryItemId } = payload;
+ const updatedInventoryItems = state.inventoryItems.map((item) =>
+ item.id === inventoryItemId
+ ? {
+ ...item,
+ images: item.images?.filter((img) => img.id !== fileId) ?? []
+ }
+ : item
+ );
+ return { ...state, inventoryItems: updatedInventoryItems };
+ }
default:
return state;
}
diff --git a/src/reducers/sponsors_inventory/inventory-item-reducer.js b/src/reducers/sponsors_inventory/inventory-item-reducer.js
index 7ecf1753a..58425540a 100644
--- a/src/reducers/sponsors_inventory/inventory-item-reducer.js
+++ b/src/reducers/sponsors_inventory/inventory-item-reducer.js
@@ -185,10 +185,8 @@ const inventoryItemReducer = (state = DEFAULT_STATE, action) => {
};
}
case INVENTORY_ITEM_IMAGE_DELETED: {
- const { imageId } = payload;
- const images = state.entity.images.filter(
- (image) => image.id !== imageId
- );
+ const { fileId } = payload;
+ const images = state.entity.images.filter((image) => image.id !== fileId);
return {
...state,
entity: {