From eae11682b8ae77cbff395564f561d13117581261 Mon Sep 17 00:00:00 2001 From: Roman Hotsiy Date: Thu, 16 Aug 2018 09:56:47 +0300 Subject: [PATCH] chore: sections/markdown refactor --- src/common-elements/panels.ts | 32 +++++- src/common-elements/perfect-scrollbar.tsx | 37 ++++++- src/components/ApiInfo/ApiDescription.tsx | 22 ---- src/components/ApiInfo/ApiInfo.tsx | 77 +++++++------- src/components/ApiInfo/index.ts | 1 - src/components/ContentItems/ContentItems.tsx | 84 +++++++++------ src/components/Markdown/AdvancedMarkdown.tsx | 38 +++++++ src/components/Markdown/Markdown.tsx | 100 +++--------------- src/components/Markdown/SanitizedMdBlock.tsx | 30 ++++++ ...styled.elements.ts => styled.elements.tsx} | 0 src/components/Operation/Operation.tsx | 12 +-- src/components/Redoc/Redoc.tsx | 3 +- src/components/SearchBox/SearchBox.tsx | 37 ++++--- src/components/SearchBox/styled.elements.tsx | 1 - .../SecurityRequirement.tsx | 35 +++--- .../SecuritySchemes/SecuritySchemes.tsx | 18 ++-- src/components/SideMenu/SideMenu.tsx | 42 +++----- src/components/SideMenu/styled.elements.ts | 10 +- src/components/index.ts | 1 - src/index.ts | 2 +- 20 files changed, 306 insertions(+), 276 deletions(-) delete mode 100644 src/components/ApiInfo/ApiDescription.tsx create mode 100644 src/components/Markdown/AdvancedMarkdown.tsx create mode 100644 src/components/Markdown/SanitizedMdBlock.tsx rename src/components/Markdown/{styled.elements.ts => styled.elements.tsx} (100%) diff --git a/src/common-elements/panels.ts b/src/common-elements/panels.ts index ffe5202e..37cfcb0d 100644 --- a/src/common-elements/panels.ts +++ b/src/common-elements/panels.ts @@ -1,19 +1,44 @@ -import styled, { media } from '../styled-components'; +import { SECTION_ATTR } from '../services/MenuStore'; +import styled, { media, withProps } from '../styled-components'; export const MiddlePanel = styled.div` width: calc(100% - ${props => props.theme.rightPanel.width}); - padding: ${props => props.theme.spacing.unit * 8}px; + padding: 0 ${props => props.theme.spacing.unit * 8}px; ${media.lessThan('medium')` width: 100%; `}; `; +export const Section = withProps<{ underlined?: boolean }>( + styled.div.attrs({ + [SECTION_ATTR]: props => props.id, + } as any), +)` + padding: ${props => props.theme.spacing.unit * 8}px 0; + + ${props => + (props.underlined && + ` + position: relative; + + &:not(:last-of-type):after { + position: absolute; + bottom: 0; + width: 100%; + display: block; + content: ''; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + } + `) || + ''} +`; + export const RightPanel = styled.div` width: ${props => props.theme.rightPanel.width}; color: #fafbfc; background-color: ${props => props.theme.rightPanel.backgroundColor}; - padding: ${props => props.theme.spacing.unit * 8}px; + padding: 0 ${props => props.theme.spacing.unit * 8}px; ${media.lessThan('medium')` width: 100%; @@ -27,6 +52,7 @@ export const DarkRightPanel = RightPanel.extend` export const Row = styled.div` display: flex; width: 100%; + padding: 0; ${media.lessThan('medium')` flex-direction: column; diff --git a/src/common-elements/perfect-scrollbar.tsx b/src/common-elements/perfect-scrollbar.tsx index 8695eacd..5025f3dd 100644 --- a/src/common-elements/perfect-scrollbar.tsx +++ b/src/common-elements/perfect-scrollbar.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import PerfectScrollbarType, * as PerfectScrollbarNamespace from 'perfect-scrollbar'; import psStyles from 'perfect-scrollbar/css/perfect-scrollbar.css'; + +import { OptionsContext } from '../components/OptionsProvider'; import styled, { injectGlobal } from '../styled-components'; /* @@ -18,11 +20,13 @@ const StyledScrollWrapper = styled.div` position: relative; `; -export class PerfectScrollbar extends React.Component<{ +export interface PerfectScrollbarProps { options?: PerfectScrollbarType.Options; className?: string; - updateFn: (fn) => void; -}> { + updateFn?: (fn) => void; +} + +export class PerfectScrollbar extends React.Component { private _container: HTMLElement; private inst: PerfectScrollbarType; @@ -49,7 +53,9 @@ export class PerfectScrollbar extends React.Component<{ render() { const { children, className, updateFn } = this.props; - updateFn(this.componentDidUpdate.bind(this)); + if (updateFn) { + updateFn(this.componentDidUpdate.bind(this)); + } return ( @@ -58,3 +64,26 @@ export class PerfectScrollbar extends React.Component<{ ); } } + +export function PerfectScrollbarWrap( + props: PerfectScrollbarProps & { children: JSX.Element[] | JSX.Element }, +) { + return ( + + {options => + !options.nativeScrollbars ? ( + {props.children} + ) : ( +
+ {props.children} +
+ ) + } +
+ ); +} diff --git a/src/components/ApiInfo/ApiDescription.tsx b/src/components/ApiInfo/ApiDescription.tsx deleted file mode 100644 index 0696578e..00000000 --- a/src/components/ApiInfo/ApiDescription.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; - -import { MiddlePanel, Row } from '../../common-elements/'; - -import { Markdown } from '../Markdown/Markdown'; - -export interface ApiDescriptionProps { - description: string; -} - -export class ApiDescription extends React.PureComponent { - render() { - const { description } = this.props; - return ( - - - - - - ); - } -} diff --git a/src/components/ApiInfo/ApiInfo.tsx b/src/components/ApiInfo/ApiInfo.tsx index 26aaa8c3..80d913a3 100644 --- a/src/components/ApiInfo/ApiInfo.tsx +++ b/src/components/ApiInfo/ApiInfo.tsx @@ -3,8 +3,8 @@ import * as React from 'react'; import { AppStore } from '../../services/AppStore'; -import { MiddlePanel, Row } from '../../common-elements/'; - +import { MiddlePanel, Row, Section } from '../../common-elements/'; +import { Markdown } from '../Markdown/Markdown'; import { StyledMarkdownBlock } from '../Markdown/styled.elements'; import { ApiHeader, @@ -70,43 +70,46 @@ export class ApiInfo extends React.Component { null; return ( - - - - {info.title} ({info.version}) - - {!hideDownloadButton && ( -

- Download OpenAPI specification: - - Download - -

- )} - - {((info.license || info.contact || info.termsOfService) && ( - - - {email} {website} {license} {terms} - - - )) || - null} - - {(externalDocs && ( +
+ + + + {info.title} ({info.version}) + + {!hideDownloadButton && (

- {externalDocs.description || externalDocs.url} + Download OpenAPI specification: + + Download +

- )) || - null} - -
-
+ )} + + {((info.license || info.contact || info.termsOfService) && ( + + + {email} {website} {license} {terms} + + + )) || + null} + + {(externalDocs && ( +

+ {externalDocs.description || externalDocs.url} +

+ )) || + null} +
+ + + +
); } } diff --git a/src/components/ApiInfo/index.ts b/src/components/ApiInfo/index.ts index c82a5768..07ab5e7c 100644 --- a/src/components/ApiInfo/index.ts +++ b/src/components/ApiInfo/index.ts @@ -1,2 +1 @@ -export { ApiDescription } from './ApiDescription'; export { ApiInfo } from './ApiInfo'; diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 6deba5c0..f7441990 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -1,10 +1,9 @@ import { observer } from 'mobx-react'; import * as React from 'react'; -import { SECTION_ATTR } from '../../services/MenuStore'; -import { Markdown } from '../Markdown/Markdown'; +import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown'; -import { H1, H2, MiddlePanel, Row, ShareLink } from '../../common-elements'; +import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { MDXComponentMeta } from '../../services/MarkdownRenderer'; import { ContentItemModel } from '../../services/MenuBuilder'; import { GroupModel, OperationModel } from '../../services/models'; @@ -12,20 +11,22 @@ import { Operation } from '../Operation/Operation'; import { SecurityDefs } from '../SecuritySchemes/SecuritySchemes'; import { StoreConsumer } from '../StoreBuilder'; +const DEFAULT_ALLOWED_COMPONENTS = { + 'security-definitions': { + component: SecurityDefs, + propsSelector: _store => ({ + securitySchemes: _store!.spec.securitySchemes, + }), + }, +}; + @observer export class ContentItems extends React.Component<{ items: ContentItemModel[]; allowedMdComponents?: Dict; }> { static defaultProps = { - allowedMdComponents: { - 'security-definitions': { - component: SecurityDefs, - propsSelector: _store => ({ - securitySchemes: _store!.spec.securitySchemes, - }), - }, - }, + allowedMdComponents: DEFAULT_ALLOWED_COMPONENTS, }; render() { @@ -34,14 +35,18 @@ export class ContentItems extends React.Component<{ return null; } return items.map(item => ( - + )); } } export interface ContentItemProps { item: ContentItemModel; - allowedMdComponents?: Dict; + allowedMdComponents: Dict; } @observer @@ -67,15 +72,21 @@ export class ContentItem extends React.Component { throw new Error('Unknown item type'); } - return [ -
- {content} -
, - (item as any).items && , - ]; + return ( + <> +
+ {content} +
+ {item.items && ( + + )} + + ); } } +const middlePanelWrap = component => {component}; + @observer export class SectionItem extends React.Component { render() { @@ -83,23 +94,26 @@ export class SectionItem extends React.Component { const components = this.props.allowedMdComponents; const Header = level === 2 ? H2 : H1; return ( - - -
- - {name} -
- {components ? ( - - {store => ( - - )} - - ) : ( - + <> + + +
+ + {name} +
+
+
+ + {store => ( + )} -
-
+ + ); } } diff --git a/src/components/Markdown/AdvancedMarkdown.tsx b/src/components/Markdown/AdvancedMarkdown.tsx new file mode 100644 index 00000000..5b4a1f90 --- /dev/null +++ b/src/components/Markdown/AdvancedMarkdown.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services'; +import { BaseMarkdownProps } from './Markdown'; +import { SanitizedMarkdownHTML } from './SanitizedMdBlock'; + +export interface AdvancedMarkdownProps extends BaseMarkdownProps { + store?: AppStore; + allowedComponents: Dict; + htmlWrap?: (part: JSX.Element) => JSX.Element; +} + +export class AdvancedMarkdown extends React.Component { + render() { + const { store, source, allowedComponents, htmlWrap = i => i } = this.props; + + if (!store) { + throw new Error('When using componentes in markdown, store prop must be provided'); + } + + const renderer = new MarkdownRenderer(); + const parts = renderer.renderMdWithComponents(source, allowedComponents); + + if (!parts.length) { + return null; + } + + return parts.map((part, idx) => { + if (typeof part === 'string') { + return React.cloneElement( + htmlWrap(), + { key: idx }, + ); + } + return ; + }); + } +} diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index c5233227..0259b04e 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/src/components/Markdown/Markdown.tsx @@ -1,103 +1,35 @@ import * as React from 'react'; -import * as DOMPurify from 'dompurify'; -import { AppStore, MarkdownRenderer, MDXComponentMeta } from '../../services'; -import { OptionsContext } from '../OptionsProvider'; - -import { StyledMarkdownBlock } from './styled.elements'; - -const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span'); +import { MarkdownRenderer } from '../../services'; +import { SanitizedMarkdownHTML } from './SanitizedMdBlock'; export interface StylingMarkdownProps { dense?: boolean; inline?: boolean; } -export interface BaseMarkdownProps extends StylingMarkdownProps { +export interface BaseMarkdownProps { sanitize?: boolean; - store?: AppStore; -} - -const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); - -function SanitizedMarkdownHTML(props: StylingMarkdownProps & { html: string }) { - const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock; - - return ( - - {options => ( - - )} - - ); -} - -export interface MarkdownProps extends BaseMarkdownProps { - allowedComponents?: Dict; source: string; } +export type MarkdownProps = BaseMarkdownProps & + StylingMarkdownProps & { + source: string; + className?: string; + }; + export class Markdown extends React.Component { - constructor(props: MarkdownProps) { - super(props); - - if (props.allowedComponents && props.inline) { - throw new Error('Markdown Component: "inline" mode doesn\'t support "components"'); - } - } - render() { - const { source, allowedComponents, store, inline, dense } = this.props; - - if (allowedComponents && !store) { - throw new Error('When using componentes in markdown, store prop must be provided'); - } - + const { source, inline, dense, className } = this.props; const renderer = new MarkdownRenderer(); - if (allowedComponents) { - return ( - - ); - } else { - return ( - - ); - } - } -} - -export interface AdvancedMarkdownProps extends BaseMarkdownProps { - parts: Array; -} - -export class AdvancedMarkdown extends React.Component { - render() { - const { inline, dense, store, parts } = this.props; - - if (!parts.length) { - return null; - } - return ( - <> - {parts.map( - (part, idx) => - typeof part === 'string' ? ( - - ) : ( - - ), - )} - + ); } } diff --git a/src/components/Markdown/SanitizedMdBlock.tsx b/src/components/Markdown/SanitizedMdBlock.tsx new file mode 100644 index 00000000..30903ddf --- /dev/null +++ b/src/components/Markdown/SanitizedMdBlock.tsx @@ -0,0 +1,30 @@ +import * as DOMPurify from 'dompurify'; +import * as React from 'react'; + +import { OptionsContext } from '../OptionsProvider'; +import { StylingMarkdownProps } from './Markdown'; +import { StyledMarkdownBlock } from './styled.elements'; + +const StyledMarkdownSpan = StyledMarkdownBlock.withComponent('span'); + +const sanitize = (untrustedSpec, html) => (untrustedSpec ? DOMPurify.sanitize(html) : html); + +export function SanitizedMarkdownHTML( + props: StylingMarkdownProps & { html: string; className?: string }, +) { + const Wrap = props.inline ? StyledMarkdownSpan : StyledMarkdownBlock; + + return ( + + {options => ( + + )} + + ); +} diff --git a/src/components/Markdown/styled.elements.ts b/src/components/Markdown/styled.elements.tsx similarity index 100% rename from src/components/Markdown/styled.elements.ts rename to src/components/Markdown/styled.elements.tsx diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 1f5cb108..2fa7f748 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -23,20 +23,10 @@ const OperationRow = Row.extend` contain: content; overflow: hidden; - position: relative; - - &:after { - position: absolute; - bottom: 0; - width: 100%; - display: block; - content: ''; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); - } `; const Description = styled(Markdown)` - margin-bottom: ${({ theme }) => theme.spacing.unit * 8}; + margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; `; export interface OperationProps { diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 023e5244..323cdbd4 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { ThemeProvider } from '../../styled-components'; import { AppStore } from '../../services'; -import { ApiDescription, ApiInfo } from '../ApiInfo/'; +import { ApiInfo } from '../ApiInfo/'; import { ApiLogo } from '../ApiLogo/ApiLogo'; import { ContentItems } from '../ContentItems/ContentItems'; import { OptionsProvider } from '../OptionsProvider'; @@ -57,7 +57,6 @@ export class Redoc extends React.Component { - diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 0715a728..e6ade2c3 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -7,6 +7,7 @@ import { MenuItem } from '../SideMenu/MenuItem'; import { MarkerService } from '../../services/MarkerService'; import { SearchResult } from '../../services/SearchWorker.worker'; +import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { ClearIcon, SearchIcon, @@ -135,21 +136,27 @@ export class SearchBox extends React.PureComponent {results.length > 0 && ( - - {results.map((res, idx) => ( - - ))} - + + + {results.map((res, idx) => ( + + ))} + + )} ); diff --git a/src/components/SearchBox/styled.elements.tsx b/src/components/SearchBox/styled.elements.tsx index 5214ccf4..89f48ec5 100644 --- a/src/components/SearchBox/styled.elements.tsx +++ b/src/components/SearchBox/styled.elements.tsx @@ -58,7 +58,6 @@ export const SearchResultsBox = styled.div` margin-top: 10px; line-height: 1.4; font-size: 0.9em; - overflow: auto; ${MenuItemLabel} { padding-top: 6px; diff --git a/src/components/SecurityRequirement/SecurityRequirement.tsx b/src/components/SecurityRequirement/SecurityRequirement.tsx index 2e872543..cba33f36 100644 --- a/src/components/SecurityRequirement/SecurityRequirement.tsx +++ b/src/components/SecurityRequirement/SecurityRequirement.tsx @@ -82,20 +82,23 @@ export class SecurityRequirement extends React.PureComponent props.theme.schema.defaultDetailsWidth}; `; const AuthHeader = UnderlinedHeader.extend` display: inline-block; + margin: 0; `; -const Table = styled.table` +const Wrap = styled.div` width: 100%; - border-collapse: collapse; - font-size: inherit; + display: flex; + margin: 1em 0; `; export interface SecurityRequirementsProps { @@ -109,20 +112,14 @@ export class SecurityRequirements extends React.PureComponent - - - - Authorizations: - - - {securities.map((security, idx) => ( - - ))} - - - - + + + Authorizations: + + + {securities.map((security, idx) => )} + + ); } } diff --git a/src/components/SecuritySchemes/SecuritySchemes.tsx b/src/components/SecuritySchemes/SecuritySchemes.tsx index cff9ecb4..6af18621 100644 --- a/src/components/SecuritySchemes/SecuritySchemes.tsx +++ b/src/components/SecuritySchemes/SecuritySchemes.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { SecuritySchemesModel } from '../../services/models'; -import { H2, ShareLink } from '../../common-elements'; +import { H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; import { OpenAPISecurityScheme } from '../../types'; import { Markdown } from '../Markdown/Markdown'; import { StyledMarkdownBlock } from '../Markdown/styled.elements'; @@ -66,10 +66,10 @@ export interface SecurityDefsProps { export class SecurityDefs extends React.PureComponent { render() { - return ( -
- {this.props.securitySchemes.schemes.map(scheme => ( -
+ return this.props.securitySchemes.schemes.map(scheme => ( +
+ +

{scheme.id} @@ -118,9 +118,9 @@ export class SecurityDefs extends React.PureComponent { -

- ))} -
- ); +
+
+ + )); } } diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx index bb5d00c5..bf7c8415 100644 --- a/src/components/SideMenu/SideMenu.tsx +++ b/src/components/SideMenu/SideMenu.tsx @@ -1,11 +1,10 @@ import { observer } from 'mobx-react'; import * as React from 'react'; -import { OptionsContext } from '../OptionsProvider'; import { IMenuItem, MenuStore } from '../../services/MenuStore'; import { MenuItems } from './MenuItems'; -import { PerfectScrollbar } from '../../common-elements/perfect-scrollbar'; +import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar'; import { RedocAttribution } from './styled.elements'; @observer @@ -15,31 +14,20 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str render() { const store = this.props.menu; return ( - - {options => - options.nativeScrollbars ? ( - - ) : ( - - - - - Documentation Powered by ReDoc - - - - ) - } - + + + + + Documentation Powered by ReDoc + + + ); } diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts index 6e0c8794..e332a13c 100644 --- a/src/components/SideMenu/styled.elements.ts +++ b/src/components/SideMenu/styled.elements.ts @@ -164,9 +164,10 @@ export const MenuItemTitle = withProps<{ width?: string }>(styled.span)` `; export const RedocAttribution = styled.div` + ${({ theme }) => ` font-size: 0.8em; - margin-top: ${({ theme }) => `${theme.spacing.unit * 2}px`}; - padding: ${({ theme }) => `0 ${theme.spacing.unit * 4}px`}; + margin-top: ${theme.spacing.unit * 2}px; + padding: 0 ${theme.spacing.unit * 4}px; text-align: left; opacity: 0.7; @@ -174,9 +175,10 @@ export const RedocAttribution = styled.div` a, a:visited, a:hover { - color: ${({ theme }) => theme.colors.text.primary} !important; + color: ${theme.colors.text.primary} !important; border-top: 1px solid #e1e1e1; - padding-top: 10px; + padding: ${theme.spacing.unit}px 0; display: block; } +`}; `; diff --git a/src/components/index.ts b/src/components/index.ts index 9810afcc..67ec1de0 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,7 +1,6 @@ export * from './RedocStandalone'; export * from './Redoc/Redoc'; export * from './ApiInfo/ApiInfo'; -export * from './ApiInfo/ApiDescription'; export * from './ApiLogo/ApiLogo'; export * from './ContentItems/ContentItems'; export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements'; diff --git a/src/index.ts b/src/index.ts index 98f6c473..05d9cd0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './components'; -export { MiddlePanel, Row, RightPanel } from './common-elements/'; +export { MiddlePanel, Row, RightPanel, Section } from './common-elements/'; export * from './services'; export * from './utils';