diff --git a/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx b/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx index 3a76b9c7a0..3f35cddd7a 100644 --- a/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx +++ b/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import classNames from 'classnames'; import flow from 'lodash/flow'; import { useIntl } from 'react-intl'; @@ -33,6 +33,8 @@ import commonMessages from '../../common/messages'; import './DocGenSidebar.scss'; import { DocGenTag, DocGenTemplateTagsResponse, JsonPathsMap } from './types'; +const DEFAULT_RETRIES = 10; + type ExternalProps = { enabled: boolean; getDocGenTags: () => Promise; @@ -55,13 +57,13 @@ type JsonPathsState = { const DocGenSidebar = ({ getDocGenTags }: Props) => { const { formatMessage } = useIntl(); - const [hasError, setHasError] = React.useState(false); - const [isLoading, setIsLoading] = React.useState(false); - const [tags, setTags] = React.useState({ + const [hasError, setHasError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [tags, setTags] = useState({ text: [], image: [], }); - const [jsonPaths, setJsonPaths] = React.useState({ + const [jsonPaths, setJsonPaths] = useState({ textTree: {}, imageTree: {}, }); @@ -73,7 +75,7 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { }, base); }; - const tagsToJsonPaths = (docGenTags: DocGenTag[]): JsonPathsMap => { + const tagsToJsonPaths = useCallback((docGenTags: DocGenTag[]): JsonPathsMap => { const jsonPathsMap: JsonPathsMap = {}; docGenTags.forEach(tag => { @@ -84,49 +86,58 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { }); return jsonPathsMap; - }; + }, []); - const loadTags = async () => { - setIsLoading(true); - try { - const response: DocGenTemplateTagsResponse = await getDocGenTags(); - if (response && !!response.data) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const { data } = response || []; - - // anything that is not an image tag for this view is treated as a text tag - const textTags = data?.filter(tag => tag.tag_type !== 'image') || []; - const imageTags = data?.filter(tag => tag.tag_type === 'image') || []; - setTags({ - text: textTags, - image: imageTags, - }); - setJsonPaths({ - textTree: tagsToJsonPaths(textTags), - imageTree: tagsToJsonPaths(imageTags), - }); - setHasError(false); - } else { + const loadTags = useCallback( + async (attempts = DEFAULT_RETRIES) => { + if (attempts <= 0) { + setIsLoading(false); + return; + } + setIsLoading(true); + try { + const response: DocGenTemplateTagsResponse = await getDocGenTags(); + if (response?.message) { + loadTags.call(this, attempts - 1); + } else if (response?.data) { + const { data } = response; + // anything that is not an image tag for this view is treated as a text tag + const textTags = data?.filter(tag => tag.tag_type !== 'image') || []; + const imageTags = data?.filter(tag => tag.tag_type === 'image') || []; + setTags({ + text: textTags, + image: imageTags, + }); + setJsonPaths({ + textTree: tagsToJsonPaths(textTags), + imageTree: tagsToJsonPaths(imageTags), + }); + setHasError(false); + setIsLoading(false); + } else { + setHasError(true); + setIsLoading(false); + } + } catch (error) { setHasError(true); + setIsLoading(false); } - } catch (error) { - setHasError(true); - } - setIsLoading(false); - }; - - React.useEffect(() => { - loadTags(); + }, + // disabling eslint because the getDocGenTags prop is changing very frequently // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + [tagsToJsonPaths], + ); + + useEffect(() => { + loadTags(DEFAULT_RETRIES); + }, [loadTags]); const isEmpty = tags.image.length + tags.text.length === 0; return (
- {hasError && } + {hasError && loadTags(DEFAULT_RETRIES)} />} {isLoading && ( + Promise.resolve({ + message: 'Processing tags for this file.', + }), + ) + .mockImplementationOnce(() => + Promise.resolve({ + pagination: {}, + data: mockData, + }), + ); + const noTagsMock = jest.fn().mockReturnValue(Promise.resolve({ data: [] })); +const processingTagsMock = jest.fn().mockReturnValue( + Promise.resolve({ + message: 'Processing tags for this file.', + }), +); const errorTagsMock = jest.fn().mockRejectedValue([]); const noDataMock = jest.fn().mockReturnValue(Promise.resolve({})); describe('elements/content-sidebar/DocGenSidebar', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + const renderComponent = (props = {}) => render(); @@ -72,11 +100,32 @@ describe('elements/content-sidebar/DocGenSidebar', () => { expect(await screen.findByRole('status', { name: 'Loading' })).toBeInTheDocument(); + jest.advanceTimersByTime(1000); + await waitFor(() => { expect(screen.queryByRole('status', { name: 'Loading' })).not.toBeInTheDocument(); }); }); + test('should re-trigger loadTags if the template is still processing', async () => { + renderComponent({ + getDocGenTags: processingTagsMock, + }); + + await waitFor(() => expect(processingTagsMock).toHaveBeenCalledTimes(10)); + }); + + test('should re-trigger loadTags retrigger and successfully display the tags', async () => { + renderComponent({ + getDocGenTags: processAndResolveMock, + }); + + await waitFor(() => expect(processAndResolveMock).toHaveBeenCalledTimes(2)); + const parentTag = await screen.findByText('about'); + + expect(parentTag).toBeVisible(); + }); + test('should re-trigger getDocGenTags on click on refresh button', async () => { renderComponent({ getDocGenTags: errorTagsMock, @@ -88,7 +137,7 @@ describe('elements/content-sidebar/DocGenSidebar', () => { const refreshButton = screen.getByRole('button', { name: 'Process document' }); fireEvent.click(refreshButton); - await waitFor(() => expect(errorTagsMock).toBeCalledTimes(2)); + await waitFor(() => expect(errorTagsMock).toHaveBeenCalledTimes(2)); }); test('should handle undefined data', async () => {