-
Notifications
You must be signed in to change notification settings - Fork 347
feat(docgen-sidebar): support new types for pdf template #4508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import React, { useState, useCallback, useEffect } from 'react'; | ||
| import classNames from 'classnames'; | ||
| import flow from 'lodash/flow'; | ||
| import { useIntl } from 'react-intl'; | ||
| import { useIntl, type MessageDescriptor } from 'react-intl'; | ||
|
|
||
| import { LoadingIndicator } from '@box/blueprint-web'; | ||
|
|
||
|
|
@@ -31,10 +31,77 @@ import { WithLoggerProps } from '../../../common/types/logging'; | |
| import commonMessages from '../../common/messages'; | ||
|
|
||
| import './DocGenSidebar.scss'; | ||
| import { DocGenTag, DocGenTemplateTagsResponse, JsonPathsMap } from './types'; | ||
| import type { DocGenTag, DocGenTemplateTagsResponse, JsonPathsMap } from './types'; | ||
| import { PDF_FIELD_TAG_TYPES, isPdfFormFieldTagType } from './types'; | ||
|
|
||
| const DEFAULT_RETRIES = 10; | ||
|
|
||
| type DocGenSection = { | ||
| id: string; | ||
| message: MessageDescriptor; | ||
| tree: JsonPathsMap; | ||
| }; | ||
|
|
||
| const PDF_FIELD_TYPE_MESSAGES: Record<(typeof PDF_FIELD_TAG_TYPES)[number], MessageDescriptor> = { | ||
| checkbox: messages.checkboxTags, | ||
| radiobutton: messages.radiobuttonTags, | ||
| dropdown: messages.dropdownTags, | ||
| }; | ||
|
|
||
| const createNestedObject = (base: JsonPathsMap, paths: string[]) => { | ||
| paths.reduce((obj, path) => { | ||
| if (!obj[path]) obj[path] = {}; | ||
| return obj[path]; | ||
| }, base); | ||
| }; | ||
| const tagsToJsonPaths = (docGenTags: DocGenTag[]): JsonPathsMap => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wondering if this function will need to be used as a util in a future pr? ie should this be factored out into a utils file?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. combined these logic into one src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx |
||
| const jsonPathsMap: JsonPathsMap = {}; | ||
|
|
||
| docGenTags.forEach(tag => { | ||
| tag.json_paths.forEach(jsonPath => { | ||
| const paths = jsonPath.split('.'); | ||
| createNestedObject(jsonPathsMap, paths); | ||
| }); | ||
| }); | ||
|
|
||
| return jsonPathsMap; | ||
| }; | ||
|
|
||
| const buildDocGenSections = (data: DocGenTag[]): DocGenSection[] => { | ||
| const result: DocGenSection[] = []; | ||
| const imageTags = data.filter(tag => tag.tag_type === 'image'); | ||
| const textTags = data.filter(tag => tag.tag_type !== 'image' && !isPdfFormFieldTagType(tag.tag_type)); | ||
|
|
||
| if (textTags.length > 0) { | ||
| result.push({ | ||
| id: 'text', | ||
| message: messages.textTags, | ||
| tree: tagsToJsonPaths(textTags), | ||
| }); | ||
| } | ||
|
|
||
| if (imageTags.length > 0) { | ||
| result.push({ | ||
| id: 'image', | ||
| message: messages.imageTags, | ||
| tree: tagsToJsonPaths(imageTags), | ||
| }); | ||
| } | ||
|
|
||
| PDF_FIELD_TAG_TYPES.forEach(fieldType => { | ||
| const fieldTags = data.filter(tag => tag.tag_type === fieldType); | ||
| if (fieldTags.length > 0) { | ||
| result.push({ | ||
| id: fieldType, | ||
| message: PDF_FIELD_TYPE_MESSAGES[fieldType], | ||
| tree: tagsToJsonPaths(fieldTags), | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| return result; | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| type ExternalProps = { | ||
| enabled: boolean; | ||
| getDocGenTags: () => Promise<DocGenTemplateTagsResponse>; | ||
|
|
@@ -44,49 +111,12 @@ type ExternalProps = { | |
|
|
||
| type Props = ExternalProps & ErrorContextProps & WithLoggerProps; | ||
|
|
||
| type TagState = { | ||
| text: DocGenTag[]; | ||
| image: DocGenTag[]; | ||
| }; | ||
|
|
||
| type JsonPathsState = { | ||
| textTree: JsonPathsMap; | ||
| imageTree: JsonPathsMap; | ||
| }; | ||
|
|
||
| const DocGenSidebar = ({ getDocGenTags }: Props) => { | ||
| const { formatMessage } = useIntl(); | ||
|
|
||
| const [hasError, setHasError] = useState<boolean>(false); | ||
| const [isLoading, setIsLoading] = useState<boolean>(false); | ||
| const [tags, setTags] = useState<TagState>({ | ||
| text: [], | ||
| image: [], | ||
| }); | ||
| const [jsonPaths, setJsonPaths] = useState<JsonPathsState>({ | ||
| textTree: {}, | ||
| imageTree: {}, | ||
| }); | ||
|
|
||
| const createNestedObject = (base: JsonPathsMap, paths: string[]) => { | ||
| paths.reduce((obj, path) => { | ||
| if (!obj[path]) obj[path] = {}; | ||
| return obj[path]; | ||
| }, base); | ||
| }; | ||
|
|
||
| const tagsToJsonPaths = useCallback((docGenTags: DocGenTag[]): JsonPathsMap => { | ||
| const jsonPathsMap: JsonPathsMap = {}; | ||
|
|
||
| docGenTags.forEach(tag => { | ||
| tag.json_paths.forEach(jsonPath => { | ||
| const paths = jsonPath.split('.'); | ||
| createNestedObject(jsonPathsMap, paths); | ||
| }); | ||
| }); | ||
|
|
||
| return jsonPathsMap; | ||
| }, []); | ||
| const [sections, setSections] = useState<DocGenSection[]>([]); | ||
|
|
||
| const loadTags = useCallback( | ||
| async (attempts = DEFAULT_RETRIES) => { | ||
|
|
@@ -101,17 +131,7 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { | |
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this comment still helpful to keep?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for reminder! I moved to this line src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx
|
||
| 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), | ||
| }); | ||
| setSections(buildDocGenSections(data)); | ||
| setHasError(false); | ||
| setIsLoading(false); | ||
| } else { | ||
|
|
@@ -125,14 +145,14 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { | |
| }, | ||
| // 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; | ||
| const isEmpty = sections.length === 0; | ||
|
|
||
| return ( | ||
| <SidebarContent sidebarView={SIDEBAR_VIEW_DOCGEN} title={formatMessage(messages.docGenTags)}> | ||
|
|
@@ -147,8 +167,9 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { | |
| {!hasError && !isLoading && isEmpty && <EmptyTags />} | ||
| {!hasError && !isLoading && !isEmpty && ( | ||
| <> | ||
| <TagsSection message={messages.textTags} data={jsonPaths.textTree} /> | ||
| <TagsSection message={messages.imageTags} data={jsonPaths.imageTree} /> | ||
| {sections.map(section => ( | ||
| <TagsSection key={section.id} message={section.message} data={section.tree} /> | ||
| ))} | ||
| </> | ||
| )} | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,4 +113,32 @@ const mockData = [ | |
| }, | ||
| ]; | ||
|
|
||
| /** PDF template tags (text, checkbox, radiobutton, dropdown) */ | ||
| export const mockPdfTemplateData = [ | ||
| { | ||
| tag_type: 'text', | ||
| tag_content: '{{NameField::optional}}', | ||
| json_paths: ['NameField'], | ||
| required: false, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I notice all the other objects in this page have tag_type, tag_content, and json_paths. where does required come from? I am guessing the type for this object is defined here: https://github.com/box/box-ui-elements/blob/master/src/elements/content-sidebar/DocGenSidebar/types.ts#L2 which also does not have required. just want to make sure the types match up correctly here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch!! The property already exists but was missing in type.ts. Updated. |
||
| }, | ||
| { | ||
| tag_type: 'checkbox', | ||
| tag_content: '{{SubscribeCheckbox}}', | ||
| json_paths: ['SubscribeCheckbox'], | ||
| required: true, | ||
| }, | ||
| { | ||
| tag_type: 'radiobutton', | ||
| tag_content: '{{Gender}}', | ||
| json_paths: ['Gender'], | ||
| required: true, | ||
| }, | ||
| { | ||
| tag_type: 'dropdown', | ||
| tag_content: '{{CountryDropdown}}', | ||
| json_paths: ['CountryDropdown'], | ||
| required: true, | ||
| }, | ||
| ]; | ||
|
|
||
| export default mockData; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ import React from 'react'; | |
| import { render, screen, waitFor, fireEvent } from '../../../test-utils/testing-library'; | ||
|
|
||
| import { DocGenSidebarComponent as DocGenSidebar } from '../DocGenSidebar/DocGenSidebar'; | ||
| import mockData from '../__mocks__/DocGenSidebar.mock'; | ||
| import mockData, { mockPdfTemplateData } from '../__mocks__/DocGenSidebar.mock'; | ||
|
|
||
| const docGenSidebarProps = { | ||
| getDocGenTags: jest.fn().mockReturnValue( | ||
|
|
@@ -60,6 +60,25 @@ describe('elements/content-sidebar/DocGenSidebar', () => { | |
| expect(tagList).toHaveLength(2); | ||
| }); | ||
|
|
||
| test('should render PDF template tags in separate sections', async () => { | ||
| renderComponent({ | ||
| getDocGenTags: jest.fn().mockReturnValue( | ||
| Promise.resolve({ | ||
| pagination: {}, | ||
| data: mockPdfTemplateData, | ||
| }), | ||
| ), | ||
| }); | ||
|
|
||
| const tagList = await screen.findAllByTestId('bcs-TagsSection'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it completely necessary to use testIds? our general practice is to use semantic queries when possible like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. I followed the existing test style at first, but the behavior we care about is the rendered labels, so findByText (and getByRole when it applies) fit better than testid. I removed the |
||
| expect(tagList).toHaveLength(4); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know how important this is, but since you have checked to see if there is a length of 4 with specific mock data, are there cases where it should return 3,2,1, or even 0 or invalid tag types? I have little to no context on what the api shape is like, i'm only thinking from a testing depth perspective. maybe some sort of parameterized test to cover all the cases.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. I followed the existing test style at first, but the behavior we care about is the rendered labels, so findByText (and getByRole when it applies) fit better than testid. I removed the |
||
|
|
||
| expect(await screen.findByText('Text tags')).toBeInTheDocument(); | ||
| expect(screen.getByText('Checkbox tags')).toBeInTheDocument(); | ||
| expect(screen.getByText('Radiobutton tags')).toBeInTheDocument(); | ||
| expect(screen.getByText('Dropdown tags')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('should render DocGen sidebar component correctly with tags list', async () => { | ||
| renderComponent(); | ||
| const parentTag = await screen.findByText('about'); | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming nitpick: this function is called
createNestedObject, but as I understand it.reducedoesn't so much create a new object as much as it modifies an existing one.https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
I feel like something like
populateNestedPathsoraddNestedPathswould better communicate the mutation to future readers. What do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
combined these logic into one
buildJsonPathTreefunctionsrc/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx