Skip to content

Commit

Permalink
Merge pull request #56 from autentia/fix/181789531-prevent-remove-ima…
Browse files Browse the repository at this point in the history
…ge-updating

fix: 181789531 prevent remove image updating
  • Loading branch information
Kemil Beltre authored Apr 7, 2022
2 parents d631b14 + f54d0a3 commit 6099e0a
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 76 deletions.
59 changes: 9 additions & 50 deletions src/modules/binnacle/components/ActivityForm/ActivityForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -64,6 +65,9 @@ describe('ActivityForm', () => {
}
]

const activityFormState = container.resolve(ActivityFormState)
activityFormState.initialImageFile = 'mocked-image'

combosRepository = mock<CombosRepository>()
container.registerInstance(CombosRepository, combosRepository)

Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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<ActivitiesRepository>()
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(),
Expand All @@ -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<ActivitiesRepository>()
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)
})
})
})
Expand All @@ -995,6 +950,10 @@ function setup(activity: Activity | undefined = undefined) {
const activityFormState = container.resolve(ActivityFormState)
activityFormState.activity = activity

const getActivityImageAction = mock<GetActivityImageAction>()
container.registerInstance(GetActivityImageAction, getActivityImageAction)
getActivityImageAction.execute.mockResolvedValueOnce()

const date = chrono.now()
const mockOnAfterSubmit = jest.fn()

Expand Down
15 changes: 13 additions & 2 deletions src/modules/binnacle/components/ActivityForm/ActivityForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -25,6 +30,14 @@ export const ActivityForm: FC = () => {
handleSubmit
} = useFormContext<ActivityFormSchema>()
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)
Expand Down Expand Up @@ -130,8 +143,6 @@ export const ActivityForm: FC = () => {
)
}

// <DevTool control={control} placement="bottom-left" />

const mobileAreas = `
"start start start end end end"
"duration duration duration duration duration duration"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -33,8 +30,6 @@ function ImageField(props: Props, ref: Ref<HTMLInputElement>) {
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) {
Expand Down Expand Up @@ -66,27 +61,13 @@ function ImageField(props: Props, ref: Ref<HTMLInputElement>) {
})

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)
}
}
}

const addImage = (value: string | null) => {
props.setImageValue(value)
const addImage = (imageValue: string | null) => {
props.setImageValue(imageValue)
setHasImage(true)
}

Expand Down Expand Up @@ -129,7 +110,6 @@ function ImageField(props: Props, ref: Ref<HTMLInputElement>) {
<IconButton
data-testid="open-image"
onClick={openImage}
isLoading={isLoadingImage}
variant="ghost"
isRound={true}
size="sm"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ActivitiesRepository>()

const activityFormState = new ActivityFormState()

const expectedValue = 'image'
activitiesRepository.getActivityImage.mockResolvedValue(expectedValue)

return {
getActivityImageAction: new GetActivityImageAction(activitiesRepository, activityFormState),
activitiesRepository,
activityFormState,
expectedValue
}
}
Original file line number Diff line number Diff line change
@@ -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<number> {
constructor(
private activitiesRepository: ActivitiesRepository,
private activityFormState: ActivityFormState
) {
makeObservable(this)
}

@action
async execute(activityId?: number): Promise<void> {
const response = activityId
? await this.activitiesRepository.getActivityImage(activityId)
: null

runInAction(() => {
this.activityFormState.initialImageFile = response
})
}
}
3 changes: 3 additions & 0 deletions src/modules/binnacle/data-access/state/activity-form-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class ActivityFormState {
@observable.ref
activity?: Activity = undefined

@observable.ref
initialImageFile: string | null = null

constructor() {
makeObservable(this)
}
Expand Down

0 comments on commit 6099e0a

Please sign in to comment.