From cea06d73894e7576a96bbba73fd86000e9eb31e7 Mon Sep 17 00:00:00 2001 From: Kemil Beltre Date: Thu, 7 Apr 2022 11:18:28 +0200 Subject: [PATCH 1/4] refactor: param name --- .../components/ActivityForm/components/ImageField.tsx | 4 ++-- .../components/ActivityForm/components/ImageFieldV2.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/binnacle/components/ActivityForm/components/ImageField.tsx b/src/modules/binnacle/components/ActivityForm/components/ImageField.tsx index 370aa4a91..221d79f06 100644 --- a/src/modules/binnacle/components/ActivityForm/components/ImageField.tsx +++ b/src/modules/binnacle/components/ActivityForm/components/ImageField.tsx @@ -62,8 +62,8 @@ function ImageField(props: Props) { } } - const addImage = (value: string | null) => { - props.setImageValue(value) + const addImage = (imageValue: string | null) => { + props.setImageValue(imageValue) setHasImage(true) } diff --git a/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx b/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx index 35d7affed..94936d493 100644 --- a/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx +++ b/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx @@ -85,8 +85,8 @@ function ImageField(props: Props, ref: Ref) { } } - const addImage = (value: string | null) => { - props.setImageValue(value) + const addImage = (imageValue: string | null) => { + props.setImageValue(imageValue) setHasImage(true) } From d6648aebbdf1ba54152b3ea7a999f672a1af6ab6 Mon Sep 17 00:00:00 2001 From: Kemil Beltre Date: Thu, 7 Apr 2022 11:19:14 +0200 Subject: [PATCH 2/4] feat: add get activity image action --- .../actions/get-activity-image-action.test.ts | 31 +++++++++++++++++++ .../actions/get-activity-image-action.ts | 26 ++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/modules/binnacle/data-access/actions/get-activity-image-action.test.ts create mode 100644 src/modules/binnacle/data-access/actions/get-activity-image-action.ts diff --git a/src/modules/binnacle/data-access/actions/get-activity-image-action.test.ts b/src/modules/binnacle/data-access/actions/get-activity-image-action.test.ts new file mode 100644 index 000000000..4e7381056 --- /dev/null +++ b/src/modules/binnacle/data-access/actions/get-activity-image-action.test.ts @@ -0,0 +1,31 @@ +import { ActivitiesRepository } from 'modules/binnacle/data-access/repositories/activities-repository' +import { mock } from 'jest-mock-extended' +import { GetActivityImageAction } from './get-activity-image-action' +import { ActivityFormState } from '../state/activity-form-state' + +describe('GetActivityImageAction', () => { + it('should get the activity image', async () => { + const { getActivityImageAction, activitiesRepository, activityFormState, expectedValue } = + setup() + await getActivityImageAction.execute(1) + + expect(activitiesRepository.getActivityImage).toHaveBeenCalledWith(1) + expect(activityFormState.initialImageFile).toBe(expectedValue) + }) +}) + +function setup() { + const activitiesRepository = mock() + + const activityFormState = new ActivityFormState() + + const expectedValue = 'image' + activitiesRepository.getActivityImage.mockResolvedValue(expectedValue) + + return { + getActivityImageAction: new GetActivityImageAction(activitiesRepository, activityFormState), + activitiesRepository, + activityFormState, + expectedValue + } +} diff --git a/src/modules/binnacle/data-access/actions/get-activity-image-action.ts b/src/modules/binnacle/data-access/actions/get-activity-image-action.ts new file mode 100644 index 000000000..41b271d71 --- /dev/null +++ b/src/modules/binnacle/data-access/actions/get-activity-image-action.ts @@ -0,0 +1,26 @@ +import { ActivitiesRepository } from '../repositories/activities-repository' +import { action, makeObservable, runInAction } from 'mobx' +import { singleton } from 'tsyringe' +import { IAction } from '../../../../shared/arch/interfaces/IAction' +import { ActivityFormState } from '../state/activity-form-state' + +@singleton() +export class GetActivityImageAction implements IAction { + constructor( + private activitiesRepository: ActivitiesRepository, + private activityFormState: ActivityFormState + ) { + makeObservable(this) + } + + @action + async execute(activityId?: number): Promise { + const response = activityId + ? await this.activitiesRepository.getActivityImage(activityId) + : null + + runInAction(() => { + this.activityFormState.initialImageFile = response + }) + } +} From 12f06351d80aa8d760a38a5a202b05dd3290562d Mon Sep 17 00:00:00 2001 From: Kemil Beltre Date: Thu, 7 Apr 2022 11:20:11 +0200 Subject: [PATCH 3/4] feat: add initialImageFile as global state --- src/modules/binnacle/data-access/state/activity-form-state.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/binnacle/data-access/state/activity-form-state.ts b/src/modules/binnacle/data-access/state/activity-form-state.ts index e471b5d4c..d04a4695c 100644 --- a/src/modules/binnacle/data-access/state/activity-form-state.ts +++ b/src/modules/binnacle/data-access/state/activity-form-state.ts @@ -16,6 +16,9 @@ export class ActivityFormState { @observable.ref activity?: Activity = undefined + @observable.ref + initialImageFile: string | null = null + constructor() { makeObservable(this) } From f54d0a3a3f3549c1340ecb8acb7f8e10a5cfb81e Mon Sep 17 00:00:00 2001 From: Kemil Beltre Date: Thu, 7 Apr 2022 16:11:37 +0200 Subject: [PATCH 4/4] fix: load the image initial value if has an image --- .../ActivityForm/ActivityForm.test.tsx | 59 +++---------------- .../components/ActivityForm/ActivityForm.tsx | 15 ++++- .../ActivityForm/components/ImageFieldV2.tsx | 20 ------- 3 files changed, 22 insertions(+), 72 deletions(-) diff --git a/src/modules/binnacle/components/ActivityForm/ActivityForm.test.tsx b/src/modules/binnacle/components/ActivityForm/ActivityForm.test.tsx index 0f93bc0aa..57fdae31f 100644 --- a/src/modules/binnacle/components/ActivityForm/ActivityForm.test.tsx +++ b/src/modules/binnacle/components/ActivityForm/ActivityForm.test.tsx @@ -33,6 +33,7 @@ import { import { container } from 'tsyringe' import { GetCalendarDataAction } from 'modules/binnacle/data-access/actions/get-calendar-data-action' import { ActivitiesRepository } from 'modules/binnacle/data-access/repositories/activities-repository' +import { GetActivityImageAction } from '../../data-access/actions/get-activity-image-action' jest.mock('shared/components/FloatingLabelCombobox/FloatingLabelCombobox') @@ -64,6 +65,9 @@ describe('ActivityForm', () => { } ] + const activityFormState = container.resolve(ActivityFormState) + activityFormState.initialImageFile = 'mocked-image' + combosRepository = mock() container.registerInstance(CombosRepository, combosRepository) @@ -513,11 +517,6 @@ describe('ActivityForm', () => { userEvent.click(screen.getByRole('button', { name: /save/i })) - // await waitFor(() => { - // expect(screen.getByText('activity_api_errors.closed_project_title')).toBeInTheDocument() - // expect(screen.getByText('activity_api_errors.closed_project_description')).toBeInTheDocument() - // }) - await waitForNotification({ title: 'activity_api_errors.closed_project_title', description: 'activity_api_errors.closed_project_description' @@ -918,11 +917,7 @@ describe('ActivityForm', () => { expect(uploadImgButton).toBeInTheDocument() }) - it('should download the image base64 when the user wants to see the image', async () => { - const activitiesRepository = mock() - container.registerInstance(ActivitiesRepository, activitiesRepository) - activitiesRepository.getActivityImage.mockResolvedValue('mocked-image') - + it('should open the image base64 correctly', async () => { const activity = mockActivity({ id: 10, startDate: chrono('2020-01-01T09:15:00').getDate(), @@ -937,56 +932,16 @@ describe('ActivityForm', () => { }, hasImage: true }) - await setup(activity) - const openImgButton = await screen.findByTestId('open-image') const openMock = jest.fn() window.open = openMock - userEvent.click(openImgButton) await waitFor(() => { expect(openMock).toHaveBeenCalledTimes(1) }) - - expect(activitiesRepository.getActivityImage).toHaveBeenCalledWith(activity.id) - }) - - it('should show a notification when get image request fails', async () => { - const activitiesRepository = mock() - container.registerInstance(ActivitiesRepository, activitiesRepository) - activitiesRepository.getActivityImage.mockRejectedValue(createAxiosError(408)) - - const activity = mockActivity({ - id: 10, - startDate: chrono('2020-01-01T09:15:00').getDate(), - duration: 110, - billable: false, - organization: buildOrganization({ id: 20 }), - project: buildProject({ id: 30 }), - projectRole: { - id: 100, - name: 'Role name', - requireEvidence: true - }, - hasImage: true - }) - - setup(activity) - - const openImgButton = await screen.findByTestId('open-image') - - const openMock = jest.fn() - window.open = openMock - - userEvent.click(openImgButton) - - await waitForNotification(408) - - expect(openMock).not.toHaveBeenCalledTimes(1) - expect(activitiesRepository.getActivityImage).toHaveBeenCalledWith(activity.id) }) }) }) @@ -995,6 +950,10 @@ function setup(activity: Activity | undefined = undefined) { const activityFormState = container.resolve(ActivityFormState) activityFormState.activity = activity + const getActivityImageAction = mock() + container.registerInstance(GetActivityImageAction, getActivityImageAction) + getActivityImageAction.execute.mockResolvedValueOnce() + const date = chrono.now() const mockOnAfterSubmit = jest.fn() diff --git a/src/modules/binnacle/components/ActivityForm/ActivityForm.tsx b/src/modules/binnacle/components/ActivityForm/ActivityForm.tsx index 83f20611d..bf34b8b30 100644 --- a/src/modules/binnacle/components/ActivityForm/ActivityForm.tsx +++ b/src/modules/binnacle/components/ActivityForm/ActivityForm.tsx @@ -5,12 +5,17 @@ import SelectRoleSection from 'modules/binnacle/components/ActivityForm/componen import ImageField from 'modules/binnacle/components/ActivityForm/components/ImageFieldV2' import type { RecentRole } from 'modules/binnacle/data-access/interfaces/recent-role' import type { FC } from 'react' +import { useEffect } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { TimeField } from 'shared/components/FormFields/TimeField' import chrono from 'shared/utils/chrono' import DurationText from './components/DurationText' import { useIsMobile } from 'shared/hooks' +import { useGlobalState } from '../../../../shared/arch/hooks/use-global-state' +import { ActivityFormState } from '../../data-access/state/activity-form-state' +import { GetActivityImageAction } from '../../data-access/actions/get-activity-image-action' +import { useActionLoadable } from '../../../../shared/arch/hooks/use-action-loadable' export const ACTIVITY_FORM_ID = 'activity-form-id' @@ -25,6 +30,14 @@ export const ActivityForm: FC = () => { handleSubmit } = useFormContext() const isMobile = useIsMobile() + const { activity, initialImageFile } = useGlobalState(ActivityFormState) + const [loadInitialImage] = useActionLoadable(GetActivityImageAction) + + useEffect(() => { + if (activity?.hasImage) { + loadInitialImage(activity?.id).then(() => setValue('imageBase64', initialImageFile)) + } + }, [setValue, initialImageFile, loadInitialImage, activity?.id, activity?.hasImage]) const setImageValue = (value: string | null) => { setValue('imageBase64', value) @@ -130,8 +143,6 @@ export const ActivityForm: FC = () => { ) } -// - const mobileAreas = ` "start start start end end end" "duration duration duration duration duration duration" diff --git a/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx b/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx index 94936d493..3a9b455cd 100644 --- a/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx +++ b/src/modules/binnacle/components/ActivityForm/components/ImageFieldV2.tsx @@ -10,9 +10,6 @@ import type { Control } from 'react-hook-form' import { useWatch } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { useGlobalState } from 'shared/arch/hooks/use-global-state' -import { useShowErrorNotification } from 'shared/components/Notifications/useShowErrorNotification' -import { container } from 'tsyringe' -import { ActivitiesRepository } from 'modules/binnacle/data-access/repositories/activities-repository' import { useDropzone } from 'react-dropzone' const compressionOptions = { @@ -33,8 +30,6 @@ function ImageField(props: Props, ref: Ref) { const { t } = useTranslation() const value = useWatch({ control: props.control, name: 'imageBase64' }) const { activity } = useGlobalState(ActivityFormState) - const showErrorNotification = useShowErrorNotification() - const [isLoadingImage, setIsLoadingImage] = useState(false) const [hasImage, setHasImage] = useState(() => { if (activity?.hasImage && value === null) { @@ -66,22 +61,8 @@ function ImageField(props: Props, ref: Ref) { }) const openImage = async () => { - // user added an image if (value !== null) { openImageInTab(value) - - // the activity has image, we need to download it - } else if (activity?.hasImage) { - try { - setIsLoadingImage(true) - const image = await container.resolve(ActivitiesRepository).getActivityImage(activity.id) - props.setImageValue(image) - setIsLoadingImage(false) - openImageInTab(image) - } catch (e) { - setIsLoadingImage(false) - showErrorNotification(e) - } } } @@ -129,7 +110,6 @@ function ImageField(props: Props, ref: Ref) {