diff --git a/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx b/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx index 3f35cddd7a..3a65af9d0b 100644 --- a/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx +++ b/src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx @@ -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 => { + 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; +}; + type ExternalProps = { enabled: boolean; getDocGenTags: () => Promise; @@ -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(false); const [isLoading, setIsLoading] = useState(false); - const [tags, setTags] = useState({ - text: [], - image: [], - }); - const [jsonPaths, setJsonPaths] = useState({ - 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([]); 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 - 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 ( @@ -147,8 +167,9 @@ const DocGenSidebar = ({ getDocGenTags }: Props) => { {!hasError && !isLoading && isEmpty && } {!hasError && !isLoading && !isEmpty && ( <> - - + {sections.map(section => ( + + ))} )} diff --git a/src/elements/content-sidebar/DocGenSidebar/messages.tsx b/src/elements/content-sidebar/DocGenSidebar/messages.tsx index 7c43f97a4e..65b9c5c961 100644 --- a/src/elements/content-sidebar/DocGenSidebar/messages.tsx +++ b/src/elements/content-sidebar/DocGenSidebar/messages.tsx @@ -11,6 +11,21 @@ const messages = defineMessages({ description: 'Image tags section header', defaultMessage: 'Image tags', }, + checkboxTags: { + id: 'be.docGenSidebar.checkboxTags', + description: 'Checkbox tags section header', + defaultMessage: 'Checkbox tags', + }, + radiobuttonTags: { + id: 'be.docGenSidebar.radiobuttonTags', + description: 'Radiobutton tags section header', + defaultMessage: 'Radiobutton tags', + }, + dropdownTags: { + id: 'be.docGenSidebar.dropdownTags', + description: 'Dropdown tags section header', + defaultMessage: 'Dropdown tags', + }, docGenTags: { id: 'be.docGenSidebar.docGenTags', description: 'DocGen sidebar header', diff --git a/src/elements/content-sidebar/DocGenSidebar/stories/DocGenSidebar.stories.tsx b/src/elements/content-sidebar/DocGenSidebar/stories/DocGenSidebar.stories.tsx index 289d33f621..5043d1a88d 100644 --- a/src/elements/content-sidebar/DocGenSidebar/stories/DocGenSidebar.stories.tsx +++ b/src/elements/content-sidebar/DocGenSidebar/stories/DocGenSidebar.stories.tsx @@ -4,7 +4,7 @@ import type { HttpHandler } from 'msw'; import type { Meta } from '@storybook/react'; import ContentSidebar from '../../ContentSidebar'; import { mockFileRequest } from '../../stories/__mocks__/ContentSidebarMocks'; -import mockDocGenTags from '../../__mocks__/DocGenSidebar.mock'; +import mockDocGenTags, { mockPdfTemplateData } from '../../__mocks__/DocGenSidebar.mock'; const defaultArgs = { detailsSidebarProps: { @@ -33,6 +33,16 @@ const docGenSidebarProps = { }), }; +const docGenSidebarPdfTemplateProps = { + enabled: true, + isDocGenTemplate: true, + checkDocGenTemplate: noop, + getDocGenTags: async () => ({ + pagination: {}, + data: mockPdfTemplateData, + }), +}; + export const basic = { args: { defaultView: 'docgen', @@ -52,6 +62,25 @@ export const withModernizedBlueprint = { }, }; +export const pdfTemplate = { + args: { + defaultView: 'docgen', + docGenSidebarProps: docGenSidebarPdfTemplateProps, + }, +}; + +export const pdfTemplateWithModernizedBlueprint = { + args: { + enableModernizedComponents: true, + defaultView: 'docgen', + docGenSidebarProps: docGenSidebarPdfTemplateProps, + features: { + ...global.FEATURE_FLAGS, + previewModernization: { enabled: true }, + }, + }, +}; + const meta: Meta & { parameters: { msw: { handlers: HttpHandler[] } } } = { title: 'Elements/ContentSidebar/DocGenSidebar', component: ContentSidebar, diff --git a/src/elements/content-sidebar/DocGenSidebar/types.ts b/src/elements/content-sidebar/DocGenSidebar/types.ts index 2730d827e0..88aebc4d27 100644 --- a/src/elements/content-sidebar/DocGenSidebar/types.ts +++ b/src/elements/content-sidebar/DocGenSidebar/types.ts @@ -1,7 +1,16 @@ // our apis are in snake case export type DocGenTag = { /* eslint-disable-next-line camelcase */ - tag_type: 'text' | 'arithmetic' | 'conditional' | 'for-loop' | 'table-loop' | 'image'; + tag_type: + | 'text' + | 'arithmetic' + | 'conditional' + | 'for-loop' + | 'table-loop' + | 'image' + | 'checkbox' + | 'radiobutton' + | 'dropdown'; /* eslint-disable-next-line camelcase */ tag_content: string; /* eslint-disable-next-line camelcase */ @@ -20,3 +29,10 @@ export type DocGenTemplateTagsResponse = { export interface JsonPathsMap { [key: string]: JsonPathsMap | {}; } + +/** PDF template control tags that render in their own sidebar section. */ +export const PDF_FIELD_TAG_TYPES = ['checkbox', 'radiobutton', 'dropdown'] as const; + +export function isPdfFormFieldTagType(tagType: DocGenTag['tag_type']): boolean { + return (PDF_FIELD_TAG_TYPES as readonly DocGenTag['tag_type'][]).includes(tagType); +} diff --git a/src/elements/content-sidebar/__mocks__/DocGenSidebar.mock.ts b/src/elements/content-sidebar/__mocks__/DocGenSidebar.mock.ts index 97edb53e59..66bbae7117 100644 --- a/src/elements/content-sidebar/__mocks__/DocGenSidebar.mock.ts +++ b/src/elements/content-sidebar/__mocks__/DocGenSidebar.mock.ts @@ -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, + }, + { + 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; diff --git a/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx b/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx index 8539a66f68..4a3069a626 100644 --- a/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx +++ b/src/elements/content-sidebar/__tests__/DocGenSidebar.test.tsx @@ -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'); + expect(tagList).toHaveLength(4); + + 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');