Skip to content

Commit 6e0b9b2

Browse files
author
Gabriel Tira
committed
Implemented deleteImage API, state and tests
Reset state objects containing the deleted image Updated objects to be deleted in State Updated API export and added more tests Added docs Updated Tests
1 parent ce18560 commit 6e0b9b2

File tree

13 files changed

+341
-2
lines changed

13 files changed

+341
-2
lines changed

configs/test-utils/src/__mocks__/ky.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ export = {
2828
get: createMockKyRequestFn(),
2929
post: createMockKyRequestFn(),
3030
patch: createMockKyRequestFn(),
31+
delete: createMockKyRequestFn(),
3132
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { MonkState } from '../state';
2+
import { MonkAction, MonkActionType } from './monkAction';
3+
4+
/**
5+
* The payload of a MonkDeletedOneImagePayload.
6+
*/
7+
export interface MonkDeletedOneImagePayload {
8+
/**
9+
* The ID of the inspection to which the image was deleted.
10+
*/
11+
inspectionId: string;
12+
/**
13+
* The image ID deleted.
14+
*/
15+
imageId: string;
16+
}
17+
18+
/**
19+
* Action dispatched when an image have been deleted.
20+
*/
21+
export interface MonkDeletedOneImageAction extends MonkAction {
22+
/**
23+
* The type of the action : `MonkActionType.DELETED_ONE_IMAGE`.
24+
*/
25+
type: MonkActionType.DELETED_ONE_IMAGE;
26+
/**
27+
* The payload of the action containing the fetched entities.
28+
*/
29+
payload: MonkDeletedOneImagePayload;
30+
}
31+
32+
/**
33+
* Matcher function that matches a DeletedOneImage while also inferring its type using TypeScript's type predicate
34+
* feature.
35+
*/
36+
export function isDeletedOneImageAction(action: MonkAction): action is MonkDeletedOneImageAction {
37+
return action.type === MonkActionType.DELETED_ONE_IMAGE;
38+
}
39+
40+
/**
41+
* Reducer function for a deletedOneImage action.
42+
*/
43+
export function deletedOneImage(state: MonkState, action: MonkDeletedOneImageAction): MonkState {
44+
const { images, inspections, damages, parts, renderedOutputs, views } = state;
45+
const { payload } = action;
46+
47+
const inspection = inspections.find((value) => value.id === payload.inspectionId);
48+
if (inspection) {
49+
inspection.images = inspection.images?.filter((imageId) => imageId !== payload.imageId);
50+
}
51+
const deletedImage = images.find((image) => image.id === payload.imageId);
52+
const newImages = images.filter((image) => image.id !== payload.imageId);
53+
const newDamages = damages.map((damage) => ({
54+
...damage,
55+
relatedImages: damage.relatedImages.filter((imageId) => imageId !== payload.imageId),
56+
}));
57+
const newParts = parts.map((part) => ({
58+
...part,
59+
relatedImages: part.relatedImages.filter((imageId) => imageId !== payload.imageId),
60+
}));
61+
const newViews = views.map((view) => ({
62+
...view,
63+
renderedOutputs: view.renderedOutputs.filter(
64+
(outputId) => !deletedImage?.renderedOutputs.includes(outputId),
65+
),
66+
}));
67+
const newRenderedOutputs = renderedOutputs.filter(
68+
(output) => !deletedImage?.renderedOutputs.includes(output.id),
69+
);
70+
71+
return {
72+
...state,
73+
images: [...newImages],
74+
inspections: [...inspections],
75+
damages: [...newDamages],
76+
parts: [...newParts],
77+
renderedOutputs: [...newRenderedOutputs],
78+
views: [...newViews],
79+
};
80+
}

packages/common/src/state/actions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './monkAction';
22
export * from './resetState';
33
export * from './gotOneInspection';
44
export * from './createdOneImage';
5+
export * from './deletedOneImage';
56
export * from './updatedManyTasks';
67
export * from './updatedVehicle';
78
export * from './createdOnePricing';

packages/common/src/state/actions/monkAction.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export enum MonkActionType {
1414
* An image has been uploaded to the API.
1515
*/
1616
CREATED_ONE_IMAGE = 'created_one_image',
17+
/**
18+
* An image has been deleted.
19+
*/
20+
DELETED_ONE_IMAGE = 'deleted_one_image',
1721
/**
1822
* One or more tasks have been updated.
1923
*/

packages/common/src/state/reducer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
isDeletedOneDamageAction,
2525
isGotOneInspectionPdfAction,
2626
gotOneInspectionPdf,
27+
deletedOneImage,
28+
isDeletedOneImageAction,
2729
} from './actions';
2830
import { MonkState } from './state';
2931

@@ -43,6 +45,9 @@ export function monkReducer(state: MonkState, action: MonkAction): MonkState {
4345
if (isCreatedOneImageAction(action)) {
4446
return createdOneImage(state, action);
4547
}
48+
if (isDeletedOneImageAction(action)) {
49+
return deletedOneImage(state, action);
50+
}
4651
if (isUpdatedManyTasksAction(action)) {
4752
return updatedManyTasks(state, action);
4853
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Damage, Image, Inspection, Part, RenderedOutput, View } from '@monkvision/types';
2+
import {
3+
createEmptyMonkState,
4+
deletedOneImage,
5+
isDeletedOneImageAction,
6+
MonkActionType,
7+
MonkDeletedOneImageAction,
8+
} from '../../../src';
9+
10+
const action: MonkDeletedOneImageAction = {
11+
type: MonkActionType.DELETED_ONE_IMAGE,
12+
payload: {
13+
inspectionId: 'inspections-test',
14+
imageId: 'image-id-test',
15+
},
16+
};
17+
18+
describe('DeletedOneImage action handlers', () => {
19+
describe('Action matcher', () => {
20+
it('should return true if the action has the proper type', () => {
21+
expect(isDeletedOneImageAction({ type: MonkActionType.DELETED_ONE_IMAGE })).toBe(true);
22+
});
23+
24+
it('should return false if the action does not have the proper type', () => {
25+
expect(isDeletedOneImageAction({ type: MonkActionType.RESET_STATE })).toBe(false);
26+
});
27+
});
28+
29+
describe('Action handler', () => {
30+
it('should return a new state', () => {
31+
const state = createEmptyMonkState();
32+
expect(Object.is(deletedOneImage(state, action), state)).toBe(false);
33+
});
34+
35+
it('should delete image in the state', () => {
36+
const state = createEmptyMonkState();
37+
const outputId = 'rendered-output-id-test';
38+
const damageId = 'damage-id-test';
39+
const partId = 'part-id-test';
40+
const viewId = 'view-id-test';
41+
state.inspections.push({
42+
id: action.payload.inspectionId,
43+
images: [action.payload.imageId] as string[],
44+
} as Inspection);
45+
state.images.push({
46+
id: action.payload.imageId,
47+
inspectionId: action.payload.inspectionId,
48+
views: [viewId],
49+
renderedOutputs: [outputId],
50+
} as Image);
51+
state.damages.push({
52+
id: damageId,
53+
relatedImages: [action.payload.imageId],
54+
inspectionId: action.payload.inspectionId,
55+
} as Damage);
56+
state.parts.push({
57+
id: partId,
58+
relatedImages: [action.payload.imageId],
59+
inspectionId: action.payload.inspectionId,
60+
} as Part);
61+
state.renderedOutputs.push({
62+
id: outputId,
63+
baseImageId: action.payload.imageId,
64+
} as RenderedOutput);
65+
state.views.push({
66+
id: viewId,
67+
renderedOutputs: [outputId],
68+
} as View);
69+
const newState = deletedOneImage(state, action);
70+
const inspectionImages = newState.inspections.find(
71+
(ins) => ins.id === action.payload.inspectionId,
72+
)?.images;
73+
74+
expect(inspectionImages?.length).toBe(0);
75+
expect(inspectionImages).not.toContainEqual(action.payload.imageId);
76+
expect(newState.images.length).toBe(0);
77+
expect(newState.damages.length).toBe(1);
78+
expect(newState.parts.length).toBe(1);
79+
expect(newState.views.length).toBe(1);
80+
expect(
81+
newState.damages.find((damage) => damage.relatedImages.includes(action.payload.imageId)),
82+
).toBeUndefined();
83+
expect(
84+
newState.parts.find((part) => part.relatedImages.includes(action.payload.imageId)),
85+
).toBeUndefined();
86+
expect(newState.renderedOutputs.length).toBe(0);
87+
expect(
88+
newState.renderedOutputs.find((output) => output.baseImageId === action.payload.imageId),
89+
).toBeUndefined();
90+
expect(
91+
newState.views.find((view) =>
92+
view.renderedOutputs.find((id) => id === action.payload.imageId),
93+
),
94+
).toBeUndefined();
95+
});
96+
});
97+
});

packages/common/test/state/reducer.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
jest.mock('../../src/state/actions', () => ({
22
isCreatedOneImageAction: jest.fn(() => false),
3+
isDeletedOneImageAction: jest.fn(() => false),
34
isGotOneInspectionAction: jest.fn(() => false),
45
isResetStateAction: jest.fn(() => false),
56
isUpdatedManyTasksAction: jest.fn(() => false),
@@ -12,6 +13,7 @@ jest.mock('../../src/state/actions', () => ({
1213
isUpdatedVehicleAction: jest.fn(() => false),
1314
isGotOneInspectionPdfAction: jest.fn(() => false),
1415
createdOneImage: jest.fn(() => null),
16+
deletedOneImage: jest.fn(() => null),
1517
gotOneInspection: jest.fn(() => null),
1618
resetState: jest.fn(() => null),
1719
updatedManyTasks: jest.fn(() => null),
@@ -27,6 +29,7 @@ jest.mock('../../src/state/actions', () => ({
2729

2830
import {
2931
createdOneImage,
32+
deletedOneImage,
3033
gotOneInspection,
3134
createdOnePricing,
3235
deletedOnePricing,
@@ -37,6 +40,7 @@ import {
3740
updatedVehicle,
3841
gotOneInspectionPdf,
3942
isCreatedOneImageAction,
43+
isDeletedOneImageAction,
4044
isGotOneInspectionAction,
4145
isResetStateAction,
4246
isUpdatedManyTasksAction,
@@ -59,6 +63,7 @@ const actions = [
5963
{ matcher: isResetStateAction, handler: resetState, noParams: true },
6064
{ matcher: isGotOneInspectionAction, handler: gotOneInspection },
6165
{ matcher: isCreatedOneImageAction, handler: createdOneImage },
66+
{ matcher: isDeletedOneImageAction, handler: deletedOneImage },
6267
{ matcher: isUpdatedManyTasksAction, handler: updatedManyTasks },
6368
{ matcher: isCreatedOnePricingAction, handler: createdOnePricing },
6469
{ matcher: isDeletedOnePricingAction, handler: deletedOnePricing },

packages/network/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,21 @@ been created in the API.
7070
|-----------|-----------------|----------------------------------------------------------|----------|
7171
| options | AddImageOptions | The options used to specify how to upload the image. | ✔️ |
7272

73+
### deleteImage
74+
75+
```typescript
76+
import { MonkApi } from '@monkvision/network';
77+
78+
MonkApi.deleteImage(options,apiConfig, dispatch);
79+
```
80+
81+
Delete an image from an inspection. The resulting action of this request will contain the ID of the image that has
82+
been deleted in the API.
83+
84+
| Parameter | Type | Description | Required |
85+
| ----------|--------------------|----------------------------------------------------|----------|
86+
| options | DeleteImageOptions | The options used to specify which image to delete. | ✔️ |
87+
7388
### updateTaskStatus
7489
```typescript
7590
import { MonkApi } from '@monkvision/network';

packages/network/src/api/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
getAllInspections,
66
getAllInspectionsCount,
77
} from './inspection';
8-
import { addImage } from './image';
8+
import { addImage, deleteImage } from './image';
99
import { startInspectionTasks, updateTaskStatus } from './task';
1010
import { getLiveConfig } from './liveConfigs';
1111
import { updateInspectionVehicle } from './vehicle';
@@ -23,6 +23,7 @@ export const MonkApi = {
2323
getAllInspectionsCount,
2424
createInspection,
2525
addImage,
26+
deleteImage,
2627
updateTaskStatus,
2728
startInspectionTasks,
2829
getLiveConfig,

packages/network/src/api/image/requests.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
MonkActionType,
66
MonkCreatedOneImageAction,
77
vehiclePartLabels,
8+
MonkDeletedOneImageAction,
89
} from '@monkvision/common';
910
import {
1011
ComplianceOptions,
@@ -15,14 +16,21 @@ import {
1516
ImageType,
1617
MonkEntityType,
1718
MonkPicture,
19+
ProgressStatus,
1820
TaskName,
1921
TranslationObject,
2022
VehiclePart,
2123
} from '@monkvision/types';
2224
import { v4 } from 'uuid';
2325
import { labels, sights } from '@monkvision/sights';
2426
import { getDefaultOptions, MonkApiConfig } from '../config';
25-
import { ApiCenterOnElement, ApiImage, ApiImagePost, ApiImagePostTask } from '../models';
27+
import {
28+
ApiCenterOnElement,
29+
ApiIdColumn,
30+
ApiImage,
31+
ApiImagePost,
32+
ApiImagePostTask,
33+
} from '../models';
2634
import { MonkApiResponse } from '../types';
2735
import { mapApiImage } from './mappers';
2836

@@ -593,3 +601,55 @@ export async function addImage(
593601
throw err;
594602
}
595603
}
604+
605+
/**
606+
* Options specified when deleting an image from an inspection.
607+
*/
608+
export interface DeleteImageOptions {
609+
/**
610+
* The ID of the inspection to update via the API.
611+
*/
612+
id: string;
613+
/**
614+
* Image ID that will be deleted.
615+
*/
616+
imageId: string;
617+
}
618+
619+
/**
620+
* Delete an image from an inspection.
621+
*
622+
* @param options Deletion options for the image.
623+
* @param config The API config.
624+
* @param [dispatch] Optional MonkState dispatch function that you can pass if you want this request to handle React
625+
* state management for you.
626+
*/
627+
export async function deleteImage(
628+
options: DeleteImageOptions,
629+
config: MonkApiConfig,
630+
dispatch?: Dispatch<MonkDeletedOneImageAction>,
631+
): Promise<MonkApiResponse> {
632+
const kyOptions = getDefaultOptions(config);
633+
try {
634+
const response = await ky.delete(`inspections/${options.id}/images/${options.imageId}`, {
635+
...kyOptions,
636+
json: { authorized_tasks_statuses: [ProgressStatus.NOT_STARTED] },
637+
});
638+
const body = await response.json<ApiIdColumn>();
639+
dispatch?.({
640+
type: MonkActionType.DELETED_ONE_IMAGE,
641+
payload: {
642+
inspectionId: options.id,
643+
imageId: body.id,
644+
},
645+
});
646+
return {
647+
id: body.id,
648+
response,
649+
body,
650+
};
651+
} catch (err) {
652+
console.error(`Failed to delete image: ${(err as Error).message}`);
653+
throw err;
654+
}
655+
}

0 commit comments

Comments
 (0)