Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 74 additions & 55 deletions src/elements/content-sidebar/DocGenSidebar/DocGenSidebar.tsx
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';

Expand Down Expand Up @@ -31,10 +31,75 @@ 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 buildJsonPathTree = (docGenTags: DocGenTag[]): JsonPathsMap => {
const pathTree: JsonPathsMap = {};

docGenTags.forEach(tag => {
tag.json_paths.forEach(jsonPath => {
const segments = jsonPath.split('.');
segments.reduce((node, segment) => {
if (!node[segment]) node[segment] = {};
return node[segment];
}, pathTree);
});
});

return pathTree;
};

const buildDocGenSections = (data: DocGenTag[]): DocGenSection[] => {
const result: DocGenSection[] = [];
const imageTags = data.filter(tag => tag.tag_type === 'image');
// anything that is not an image tag or pdf tag would be treated as a text tag
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: buildJsonPathTree(textTags),
});
}

if (imageTags.length > 0) {
result.push({
id: 'image',
message: messages.imageTags,
tree: buildJsonPathTree(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: buildJsonPathTree(fieldTags),
});
}
});

return result;
};

type ExternalProps = {
enabled: boolean;
getDocGenTags: () => Promise<DocGenTemplateTagsResponse>;
Expand All @@ -44,49 +109,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) => {
Expand All @@ -101,17 +129,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 {
Expand All @@ -125,14 +143,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)}>
Expand All @@ -147,8 +165,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>
Expand Down
15 changes: 15 additions & 0 deletions src/elements/content-sidebar/DocGenSidebar/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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',
Expand All @@ -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<typeof ContentSidebar> & { parameters: { msw: { handlers: HttpHandler[] } } } = {
title: 'Elements/ContentSidebar/DocGenSidebar',
component: ContentSidebar,
Expand Down
19 changes: 18 additions & 1 deletion src/elements/content-sidebar/DocGenSidebar/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
// 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 */
json_paths: Array<string>;
required: boolean;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding :) but since required is a required type property now, does that cause any sort of breaking changes? just want to check and raise - trust you and polina on the logic of all things docgen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The required property follows the DocGen tag type format to keep consistency with the type definition. At the moment, we are not using this required field in the DocGen Sidebar for any UI behavior changes.

};

export type DocGenTemplateTagsResponse = {
Expand All @@ -20,3 +30,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);
}
Loading
Loading