From 7b85fa64fe4065ad0587ac4c68f6d18525a656c0 Mon Sep 17 00:00:00 2001 From: rayangler <27821750+rayangler@users.noreply.github.com> Date: Tue, 13 Dec 2022 11:24:29 -0500 Subject: [PATCH] DOP-3342: Implement back button (#3) --- e2e/integration/menu.e2e.ts | 12 +++++- e2e/integration/urls.e2e.ts | 3 +- src/components/Redoc/Redoc.tsx | 6 ++- src/components/Redoc/styled.elements.tsx | 2 +- .../SideMenu/SideMenuBackButton.tsx | 38 +++++++++++++++++++ src/components/SideMenu/index.ts | 1 + src/components/SideMenu/styled.elements.ts | 34 ++++++++++++++--- .../DiscriminatorDropdown.test.tsx.snap | 10 +++++ .../SecurityRequirement.test.tsx.snap | 20 +++++----- src/services/RedocNormalizedOptions.ts | 14 +++++++ 10 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 src/components/SideMenu/SideMenuBackButton.tsx diff --git a/e2e/integration/menu.e2e.ts b/e2e/integration/menu.e2e.ts index 22212e16..b5873db7 100644 --- a/e2e/integration/menu.e2e.ts +++ b/e2e/integration/menu.e2e.ts @@ -4,7 +4,13 @@ describe('Menu', () => { }); it('should have valid items count', () => { - cy.get('.menu-content').find('li').should('have.length', 34); + cy.get('.menu-content').find('li').should('have.length', 35); + }); + + it('should have a back button', () => { + cy.contains('a', 'Back to Docs') + .should('have.attr', 'href') + .and('contain', 'https://mongodb.com/docs/'); }); it('should sync active menu items while scroll', () => { @@ -17,6 +23,7 @@ describe('Menu', () => { .scrollIntoView() .wait(100) .get('[role=menuitem].active') + .scrollIntoView() .children() .last() .should('have.text', 'Add a new pet to the store') @@ -28,6 +35,7 @@ describe('Menu', () => { .scrollIntoView() .wait(100) .get('[role=menuitem].active') + .scrollIntoView() .children() .last() .should('have.text', 'Add a new pet to the store') @@ -85,7 +93,7 @@ describe('Menu', () => { cy.url().should('include', 'deletePetBy%22Id'); }); - it.only('should encode URL when the operation IDs have backslashes', () => { + it('should encode URL when the operation IDs have backslashes', () => { cy.visit('e2e/standalone-3-1.html'); cy.get('label span[title="pet"]').click({ multiple: true, force: true }); cy.get('li').contains('OperationId with backslash').click({ multiple: true, force: true }); diff --git a/e2e/integration/urls.e2e.ts b/e2e/integration/urls.e2e.ts index 9db718f0..4f905214 100644 --- a/e2e/integration/urls.e2e.ts +++ b/e2e/integration/urls.e2e.ts @@ -13,7 +13,8 @@ describe('Supporting both operation/* and parent/*/operation* urls', () => { it('should supporting parent/*/operation url', () => { cy.url().then(loc => { cy.visit(loc + '#tag/pet/operation/addPet'); - cy.get('li[data-item-id="tag/pet/operation/addPet"]').should('be.visible'); + + cy.get('li[data-item-id="tag/pet/operation/addPet"]').scrollIntoView().should('be.visible'); }); }); }); diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index 814158f7..410a1a71 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -9,7 +9,7 @@ import { AppStore } from '../../services'; import { ApiInfo } from '../ApiInfo/'; import { ApiLogo } from '../ApiLogo/ApiLogo'; import { ContentItems } from '../ContentItems/ContentItems'; -import { SideMenu } from '../SideMenu/SideMenu'; +import { SideMenu, SideMenuBackButton } from '../SideMenu'; import { StickyResponsiveSidebar } from '../StickySidebar/StickyResponsiveSidebar'; import { ApiContentWrap, @@ -55,6 +55,10 @@ export class Redoc extends React.Component { + {store.spec.info.title} {(!options.disableSearch && ( { + // Depth of menu item can dictate the styling of the component + const depth = 1; + const href = backNavigationPath ?? DEFAULT_NAVIGATION_PATH; + const text = `Back to ${siteTitle ?? ''} Docs`; + + return ( + <> + + + + + ← {text} + + + + + + + ); +}; diff --git a/src/components/SideMenu/index.ts b/src/components/SideMenu/index.ts index 90fe8716..f277a7f5 100644 --- a/src/components/SideMenu/index.ts +++ b/src/components/SideMenu/index.ts @@ -1,4 +1,5 @@ export * from './MenuItem'; export * from './MenuItems'; export * from './SideMenu'; +export * from './SideMenuBackButton'; export * from './styled.elements'; diff --git a/src/components/SideMenu/styled.elements.ts b/src/components/SideMenu/styled.elements.ts index 4fd476a7..7763d2b6 100644 --- a/src/components/SideMenu/styled.elements.ts +++ b/src/components/SideMenu/styled.elements.ts @@ -45,6 +45,15 @@ function menuItemActive( } } +// The back button and currently active labels maintain a different colors +function selectMenuLabelColor( + props: MenuItemLabelType & { theme: ResolvedThemeInterface }, +): string { + const { active, depth, isBackButton, theme } = props; + if (isBackButton) return palette.gray.dark1; + return active ? menuItemActive(depth, props, 'activeTextColor') : theme.sidebar.textColor; +} + export const MenuItemUl = styled.ul<{ expanded: boolean }>` margin: 0; padding: 0; @@ -64,6 +73,10 @@ export const MenuItemLi = styled.li<{ depth: number }>` ${props => (props.depth === 0 ? 'margin-top: 15px' : '')}; `; +export const MenuLink = styled.a` + text-decoration: none; +`; + export const menuItemDepth = { 0: css` opacity: 0.7; @@ -82,6 +95,7 @@ export interface MenuItemLabelType { depth: number; active: boolean; deprecated?: boolean; + isBackButton?: boolean; type?: string; } @@ -92,12 +106,10 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({ }), }))` cursor: pointer; - color: ${props => - props.active - ? menuItemActive(props.depth, props, 'activeTextColor') - : props.theme.sidebar.textColor}; + color: ${props => selectMenuLabelColor(props)}; margin: 0; - padding: 12.5px ${props => props.theme.spacing.unit * 4}px; + ${props => props.isBackButton && 'margin-top: 16px;'} + padding: 6px 16px; ${({ depth, type, theme }) => (type === 'section' && depth > 1 && 'padding-left: ' + theme.spacing.unit * 8 + 'px;') || ''} display: flex; @@ -112,7 +124,10 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({ ${props => (props.deprecated && deprecatedCss) || ''}; &:hover { - color: ${props => menuItemActive(props.depth, props, 'activeTextColor')}; + color: ${props => + props.isBackButton + ? palette.gray.dark1 + : menuItemActive(props.depth, props, 'activeTextColor')}; background-color: ${palette.gray.light2}; } @@ -125,6 +140,13 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({ } `; +export const MenuBreak = styled.hr` + border: unset; + border-bottom: 1px solid ${palette.gray.light2}; + margin: 16px 0; + width: 100%; +`; + export const MenuItemTitle = styled.span<{ width?: string }>` display: inline-block; vertical-align: middle; diff --git a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap index 9e9cf55f..fb5c1634 100644 --- a/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/DiscriminatorDropdown.test.tsx.snap @@ -76,6 +76,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -406,6 +407,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -723,6 +725,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -1102,6 +1105,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -1444,6 +1448,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -1757,6 +1762,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -2095,6 +2101,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView ], "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -2463,6 +2470,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -2793,6 +2801,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, @@ -3110,6 +3119,7 @@ exports[`Components SchemaView discriminator should correctly render SchemaView "minItems": undefined, "options": RedocNormalizedOptions { "allowedMdComponents": Object {}, + "customOptions": Object {}, "disableSearch": false, "downloadDefinitionUrl": undefined, "downloadFileName": undefined, diff --git a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap index ab4e5536..a7af0b4f 100644 --- a/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/SecurityRequirement.test.tsx.snap @@ -3,21 +3,21 @@ exports[`SecurityRequirement should render SecurityDefs 1`] = ` "

petstore_auth

Get access to data while protecting your account credentials. OAuth2 is also a safer and more secure way to give you access.

-
Security Scheme Type: OAuth2
Flow type: implicit
Scopes:
  • write:pets -

    modify pets in your account

    +
Security Scheme Type: OAuth2
Flow type: implicit
Scopes:
  • write:pets -

    modify pets in your account

  • read:pets -

    read your pets

    -

GitLab_PersonalAccessToken

GitLab Personal Access Token description

-
Security Scheme Type: API Key
Header parameter name: PRIVATE-TOKEN

GitLab_OpenIdConnect

GitLab OpenIdConnect description

-
Security Scheme Type: OpenID Connect

basicAuth

Security Scheme Type: HTTP
HTTP Authorization Scheme: basic
" +

GitLab_PersonalAccessToken

GitLab Personal Access Token description

+
Security Scheme Type: API Key
Header parameter name: PRIVATE-TOKEN

GitLab_OpenIdConnect

GitLab OpenIdConnect description

+
Security Scheme Type: OpenID Connect

basicAuth

Security Scheme Type: HTTP
HTTP Authorization Scheme: basic
" `; -exports[`SecurityRequirement should render authDefinition 1`] = `"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth
,"`; +exports[`SecurityRequirement should render authDefinition 1`] = `"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth
,"`; exports[`SecurityRequirement should render authDefinition 2`] = ` -"
Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth (write:petsread:pets)
OAuth2: petstore_auth

Get access to data while protecting your account credentials. +"

Authorizations:
(API Key: GitLab_PersonalAccessTokenOpenID Connect: GitLab_OpenIdConnectHTTP: basicAuth) OAuth2: petstore_auth (write:petsread:pets)
OAuth2: petstore_auth

Get access to data while protecting your account credentials. OAuth2 is also a safer and more secure way to give you access.

-
Flow type: implicit
Required scopes: write:pets read:pets
Scopes:
  • write:pets -

    modify pets in your account

    +
Flow type: implicit
Required scopes: write:pets read:pets
Scopes:
  • write:pets -

    modify pets in your account

  • read:pets -

    read your pets

    -
API Key: GitLab_PersonalAccessToken

GitLab Personal Access Token description

-
Header parameter name: PRIVATE-TOKEN
OpenID Connect: GitLab_OpenIdConnect

GitLab OpenIdConnect description

-
HTTP: basicAuth
HTTP Authorization Scheme: basic
," +
API Key: GitLab_PersonalAccessToken

GitLab Personal Access Token description

+
Header parameter name: PRIVATE-TOKEN
OpenID Connect: GitLab_OpenIdConnect

GitLab OpenIdConnect description

+
HTTP: basicAuth
HTTP Authorization Scheme: basic
," `; diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 4a219eef..f1a25545 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -6,6 +6,12 @@ import { setRedocLabels } from './Labels'; import { SideNavStyleEnum } from './types'; import type { LabelsConfigRaw, MDXComponentMeta } from './types'; +// Custom options needed by DOP. Add any custom options here. +interface CustomOptions { + backNavigationPath?: string; + siteTitle?: string; +} + export interface RedocRawOptions { theme?: ThemeInterface; scrollYOffset?: number | string | (() => number); @@ -56,6 +62,9 @@ export interface RedocRawOptions { hideFab?: boolean; minCharacterLengthToInitSearch?: number; showWebhookVerb?: boolean; + + // Custom options specific to DOP's use case + customOptions?: CustomOptions; } export function argValueToBoolean(val?: string | boolean, defaultValue?: boolean): boolean { @@ -259,6 +268,8 @@ export class RedocNormalizedOptions { nonce?: string; + customOptions?: CustomOptions; + constructor(raw: RedocRawOptions, defaults: RedocRawOptions = {}) { raw = { ...defaults, ...raw }; const hook = raw.theme && raw.theme.extensionsHook; @@ -335,5 +346,8 @@ export class RedocNormalizedOptions { this.hideFab = argValueToBoolean(raw.hideFab); this.minCharacterLengthToInitSearch = argValueToNumber(raw.minCharacterLengthToInitSearch) || 3; this.showWebhookVerb = argValueToBoolean(raw.showWebhookVerb); + + // No normalization needed for custom options at the moment. Expand if needed + this.customOptions = raw.customOptions || {}; } }