diff --git a/examples/angular/package.json b/examples/angular/package.json index 149e7248f..dd4b3cf9a 100644 --- a/examples/angular/package.json +++ b/examples/angular/package.json @@ -10,9 +10,9 @@ "url": "git+https://github.com/stoplightio/elements-starter-angular.git" }, "scripts": { - "start": "ng serve --port 4200", - "build": "ng build --configuration production", - "serve": "angular-http-server --path ./dist/angular -p 4200" + "start": "NODE_OPTIONS=--openssl-legacy-provider ng serve --port 4200", + "build": "NODE_OPTIONS=--openssl-legacy-provider ng build --configuration production", + "serve": "NODE_OPTIONS=--openssl-legacy-provider angular-http-server --path ./dist/angular -p 4200" }, "bugs": { "url": "https://github.com/stoplightio/elements-starter-angular/issues" diff --git a/examples/react-cra/package.json b/examples/react-cra/package.json index 00688a97b..c70057bb7 100644 --- a/examples/react-cra/package.json +++ b/examples/react-cra/package.json @@ -5,15 +5,15 @@ "author": "Stoplight ", "license": "Unlicense", "scripts": { - "start": "PORT=4200 react-scripts start", - "build": "react-scripts build", - "serve": "serve -s -l 4200 build" + "start": "NODE_OPTIONS=--openssl-legacy-provider PORT=4200 react-scripts start", + "build": "NODE_OPTIONS=--openssl-legacy-provider react-scripts build", + "serve": "NODE_OPTIONS=--openssl-legacy-provider serve -s -l 4200 build" }, "dependencies": { "@stoplight/elements": "^7.0.0", "@stoplight/elements-dev-portal": "^1.0.0", "react": "^17.0.2", - "react-router-dom": "^5.2.0" + "react-router-dom": "^6.28.0" }, "devDependencies": { "@types/node": "^17.0.11", diff --git a/examples/react-cra/src/App.tsx b/examples/react-cra/src/App.tsx index 66372787c..092d67c88 100644 --- a/examples/react-cra/src/App.tsx +++ b/examples/react-cra/src/App.tsx @@ -1,6 +1,6 @@ import { DevPortalProvider } from '@stoplight/elements-dev-portal'; import React, { Component } from 'react'; -import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; +import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { StoplightAPI } from './components/API'; import { Navigation } from './components/Navigation'; @@ -17,14 +17,12 @@ class App extends Component {
- - - - - - - - + + } /> + } /> + } /> + } /> +
diff --git a/examples/react-cra/src/components/API.tsx b/examples/react-cra/src/components/API.tsx index 3138c2a26..a5b7b5312 100644 --- a/examples/react-cra/src/components/API.tsx +++ b/examples/react-cra/src/components/API.tsx @@ -6,6 +6,7 @@ import React from 'react'; export const StoplightAPI: React.FC = () => { return ( diff --git a/examples/react-cra/src/components/StoplightProject.tsx b/examples/react-cra/src/components/StoplightProject.tsx index eb5d644ad..3b5d8c481 100644 --- a/examples/react-cra/src/components/StoplightProject.tsx +++ b/examples/react-cra/src/components/StoplightProject.tsx @@ -4,5 +4,12 @@ import { StoplightProject } from '@stoplight/elements-dev-portal'; import React from 'react'; export const StoplightProjectDocs: React.FC = () => { - return ; + return ( + + ); }; diff --git a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/ScrollToHashElement.tsx b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/ScrollToHashElement.tsx index a20ed0c11..370f9d516 100644 --- a/packages/elements-core/src/components/MarkdownViewer/CustomComponents/ScrollToHashElement.tsx +++ b/packages/elements-core/src/components/MarkdownViewer/CustomComponents/ScrollToHashElement.tsx @@ -82,7 +82,6 @@ export const ScrollToHashElement = ({ const element = document.getElementById(removeHashCharacter(hash)); if (element) { - // console.log(`scrollIntoView ${hash} behavior: ${behavior}, inline: ${inline}, block: ${block}`); element.scrollIntoView({ behavior: firstRun ? initialBehavior : behavior, inline: inline, diff --git a/packages/elements-core/src/components/TableOfContents/TableOfContents.tsx b/packages/elements-core/src/components/TableOfContents/TableOfContents.tsx index ab9051a06..e0cfd7485 100644 --- a/packages/elements-core/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/elements-core/src/components/TableOfContents/TableOfContents.tsx @@ -3,6 +3,7 @@ import { HttpMethod, NodeType } from '@stoplight/types'; import * as React from 'react'; import { useFirstRender } from '../../hooks/useFirstRender'; +import { resolveRelativeLink } from '../../utils/string'; import { VersionBadge } from '../Docs/HttpOperation/Badges'; import { NODE_GROUP_ICON, @@ -372,7 +373,7 @@ const Node = React.memo<{ return ( = { h4: ({ color, ...props }) => , }; +const InternalRoutes = ({ children }: { children: React.ReactNode }): JSX.Element => { + return ( + + + + {children} + + } + /> + + ); +}; + export function withRouter

(WrappedComponent: React.ComponentType

): React.FC

{ const WithRouter = (props: P) => { + const outerRouter = useInRouterContext(); const basePath = props.basePath ?? '/'; const staticRouterPath = props.staticRouterPath ?? ''; const routerType = props.router ?? 'history'; const { Router, routerProps } = useRouter(routerType, basePath, staticRouterPath); + if (!outerRouter) { + return ( + + + + + + + + ); + } + return ( - - - - - - - } - /> - - + + + ); }; diff --git a/packages/elements-core/src/index.ts b/packages/elements-core/src/index.ts index ef328bfc8..2db9515fb 100644 --- a/packages/elements-core/src/index.ts +++ b/packages/elements-core/src/index.ts @@ -43,5 +43,6 @@ export { Divider, Group, ITableOfContentsTree, Item, ParsedNode, RoutingProps, T export { isHttpOperation, isHttpService, isHttpWebhookOperation } from './utils/guards'; export { ReferenceResolver } from './utils/ref-resolving/ReferenceResolver'; export { createResolvedObject } from './utils/ref-resolving/resolvedObject'; -export { slugify } from './utils/string'; +export { slugify, resolveRelativeLink } from './utils/string'; export { createElementClass } from './web-components/createElementClass'; +export { resolveUrl } from './utils/http-spec/IServer'; diff --git a/packages/elements-core/src/utils/string.ts b/packages/elements-core/src/utils/string.ts index 64cf7c11a..f716e954a 100644 --- a/packages/elements-core/src/utils/string.ts +++ b/packages/elements-core/src/utils/string.ts @@ -9,3 +9,7 @@ export function slugify(name: string) { .replace(/^-/, '') .replace(/-$/, ''); } + +export const resolveRelativeLink = (slug?: string) => { + return slug ? slug.replace(/^\//, '') : '.'; +}; diff --git a/packages/elements-dev-portal/src/components/NodeContent/NodeContent.tsx b/packages/elements-dev-portal/src/components/NodeContent/NodeContent.tsx index 77638ce98..a986ad4c4 100644 --- a/packages/elements-dev-portal/src/components/NodeContent/NodeContent.tsx +++ b/packages/elements-dev-portal/src/components/NodeContent/NodeContent.tsx @@ -188,12 +188,13 @@ const LinkComponent: CustomComponentMapping['a'] = ({ children, href, title }) = } if (edge) { - const slug = routerKind === 'hash' ? `#/${edge.slug}` : edge.slug; + const slug = routerKind === 'hash' ? `#${route.replace(node.slug, edge.slug)}` : edge.slug; return {children}; } } - return {children}; + const fullHref = routerKind === 'hash' ? `#${route}${href}` : href; + return {children}; }; function getBundledUrl(url: string | undefined) { diff --git a/packages/elements-dev-portal/src/containers/StoplightProject.tsx b/packages/elements-dev-portal/src/containers/StoplightProject.tsx index 5b1518405..777b3adce 100644 --- a/packages/elements-dev-portal/src/containers/StoplightProject.tsx +++ b/packages/elements-dev-portal/src/containers/StoplightProject.tsx @@ -10,7 +10,17 @@ import { withStyles, } from '@stoplight/elements-core'; import * as React from 'react'; -import { Link, Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom'; +import { + Link, + Navigate, + Outlet, + Route, + Routes, + useInRouterContext, + useNavigate, + useOutletContext, + useParams, +} from 'react-router-dom'; import { BranchSelector } from '../components/BranchSelector'; import { DevPortalProvider } from '../components/DevPortalProvider'; @@ -87,17 +97,7 @@ export interface StoplightProjectProps extends RoutingProps { tryItCorsProxy?: string; } -const StoplightProjectImpl: React.FC = ({ - projectId, - hideTryIt, - hideSecurityInfo, - hideServerInfo, - hideMocking, - hideExport, - collapseTableOfContents = false, - tryItCredentialsPolicy, - tryItCorsProxy, -}) => { +const StoplightProjectImpl: React.FC = ({ projectId, collapseTableOfContents = false }) => { const { branchSlug: encodedBranchSlug = '', nodeSlug = '' } = useParams<{ branchSlug?: string; nodeSlug: string }>(); const branchSlug = decodeURIComponent(encodedBranchSlug); const navigate = useNavigate(); @@ -120,47 +120,8 @@ const StoplightProjectImpl: React.FC = ({ if (!nodeSlug && isTocFetched && tableOfContents?.items) { const firstNode = findFirstNode(tableOfContents.items); if (firstNode) { - return ; - } - } - - let elem: JSX.Element; - if (isLoadingNode || !isTocFetched) { - elem = ; - } else if (isError) { - if (nodeError instanceof ResponseError) { - if (nodeError.code === 402) { - elem = ; - } else if (nodeError.code === 403) { - elem = ; - } else { - elem = ; - } - } else { - elem = ; + return ; } - } else if (!node) { - elem = ; - } else if (node?.slug && nodeSlug !== node.slug) { - // Handle redirect to node's slug - return ; - } else { - elem = ( - <> - - - - ); } const handleTocClick = () => { @@ -180,7 +141,7 @@ const StoplightProjectImpl: React.FC = ({ branches={branches.items} onChange={branch => { const encodedBranchSlug = encodeURIComponent(branch.slug); - navigate(branch.is_default ? `/${nodeSlug}` : `/branches/${encodedBranchSlug}/${nodeSlug}`); + navigate(branch.is_default ? `${nodeSlug}` : `branches/${encodedBranchSlug}/${nodeSlug}`); }} /> ) : null} @@ -196,11 +157,69 @@ const StoplightProjectImpl: React.FC = ({ } > - {elem} + ); }; +const ProjectNode: React.FC = ({ + hideTryIt, + hideSecurityInfo, + hideServerInfo, + hideMocking, + hideExport, + tryItCredentialsPolicy, + tryItCorsProxy, +}) => { + const { branchSlug: encodedBranchSlug = '', nodeSlug = '' } = useParams<{ branchSlug?: string; nodeSlug: string }>(); + const branchSlug = decodeURIComponent(encodedBranchSlug); + const [isLoadingNode, isTocFetched, isError, nodeError, node] = useOutletContext(); + + if (isLoadingNode || !isTocFetched) { + return ; + } + + if (isError) { + if (nodeError instanceof ResponseError) { + if (nodeError.code === 402) { + return ; + } else if (nodeError.code === 403) { + return ; + } else { + return ; + } + } else { + return ; + } + } + + if (!node) { + return ; + } + + if (node?.slug && nodeSlug !== node.slug) { + // Handle redirect to node's slug + return ; + } + + return ( + <> + + + + ); +}; + const StoplightProjectRouter = ({ platformUrl, basePath = '/', @@ -209,19 +228,36 @@ const StoplightProjectRouter = ({ ...props }: StoplightProjectProps) => { const { Router, routerProps } = useRouter(router, basePath, staticRouterPath); + const outerRouter = useInRouterContext(); + + const InternalRoutes = () => ( + + }> + } /> + + } /> + + } /> + + + ); + + if (!outerRouter) { + return ( + + + + + + + + ); + } return ( - - - } /> - - } /> - - } /> - - + ); diff --git a/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx b/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx index 9cbf42287..b74d49659 100644 --- a/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx +++ b/packages/elements/src/components/API/APIWithResponsiveSidebarLayout.tsx @@ -2,6 +2,7 @@ import { ElementsOptionsProvider, ExportButtonProps, ParsedDocs, + resolveRelativeLink, ResponsiveSidebarLayout, } from '@stoplight/elements-core'; import { ExtensionAddonRenderer } from '@stoplight/elements-core/components/Docs'; @@ -10,7 +11,7 @@ import * as React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; import { ServiceNode } from '../../utils/oas/types'; -import { computeAPITree, findFirstNodeSlug, isInternal } from './utils'; +import { computeAPITree, findFirstNodeSlug, isInternal, resolveRelativePath } from './utils'; type SidebarLayoutProps = { serviceNode: ServiceNode; @@ -28,6 +29,7 @@ type SidebarLayoutProps = { tryItCorsProxy?: string; compact?: number | boolean; renderExtensionAddon?: ExtensionAddonRenderer; + basePath?: string; }; export const APIWithResponsiveSidebarLayout: React.FC = ({ @@ -46,6 +48,7 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ tryItCredentialsPolicy, tryItCorsProxy, renderExtensionAddon, + basePath = '/', }) => { const container = React.useRef(null); const tree = React.useMemo( @@ -53,10 +56,11 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ [serviceNode, hideSchemas, hideInternal], ); const location = useLocation(); - const { pathname } = location; + const { pathname: currentPath } = location; + const relativePath = resolveRelativePath(currentPath, basePath); - const isRootPath = !pathname || pathname === '/'; - const node = isRootPath ? serviceNode : serviceNode.children.find(child => child.uri === pathname); + const isRootPath = relativePath === '/'; + const node = isRootPath ? serviceNode : serviceNode.children.find(child => child.uri === relativePath); const layoutOptions = React.useMemo( () => ({ @@ -76,12 +80,12 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ const firstSlug = findFirstNodeSlug(tree); if (firstSlug) { - return ; + return ; } } if (hideInternal && node && isInternal(node)) { - return ; + return ; } const handleTocClick = () => { @@ -101,8 +105,8 @@ export const APIWithResponsiveSidebarLayout: React.FC = ({ {node && ( = ({ @@ -50,6 +52,7 @@ export const APIWithSidebarLayout: React.FC = ({ tryItCredentialsPolicy, tryItCorsProxy, renderExtensionAddon, + basePath = '/', }) => { const container = React.useRef(null); const tree = React.useMemo( @@ -57,9 +60,10 @@ export const APIWithSidebarLayout: React.FC = ({ [serviceNode, hideSchemas, hideInternal], ); const location = useLocation(); - const { pathname } = location; - const isRootPath = !pathname || pathname === '/'; - const node = isRootPath ? serviceNode : serviceNode.children.find(child => child.uri === pathname); + const { pathname: currentPath } = location; + const relativePath = resolveRelativePath(currentPath, basePath); + const isRootPath = relativePath === '/'; + const node = isRootPath ? serviceNode : serviceNode.children.find(child => child.uri === relativePath); const layoutOptions = React.useMemo( () => ({ @@ -78,16 +82,16 @@ export const APIWithSidebarLayout: React.FC = ({ const firstSlug = findFirstNodeSlug(tree); if (firstSlug) { - return ; + return ; } } if (hideInternal && node && isInternal(node)) { - return ; + return ; } const sidebar = ( - + ); return ( @@ -95,8 +99,8 @@ export const APIWithSidebarLayout: React.FC = ({ {node && ( ( } }); }; + +export const resolveRelativePath = (currentPath: string, basePath: string): string => { + if (!basePath || basePath === '/') { + return currentPath; + } + const baseUrl = resolveUrl(basePath); + const currentUrl = resolveUrl(currentPath); + return baseUrl && currentUrl && baseUrl !== currentUrl ? currentUrl.replace(baseUrl, '') : '/'; +}; diff --git a/packages/elements/src/containers/API.tsx b/packages/elements/src/containers/API.tsx index 458aa97b6..397e889a4 100644 --- a/packages/elements/src/containers/API.tsx +++ b/packages/elements/src/containers/API.tsx @@ -145,6 +145,7 @@ export const APIImpl: React.FC = props => { tryItCorsProxy, maxRefDepth, renderExtensionAddon, + basePath, } = props; const location = useLocation(); const apiDescriptionDocument = propsAreWithDocument(props) ? props.apiDescriptionDocument : undefined; @@ -235,6 +236,7 @@ export const APIImpl: React.FC = props => { tryItCredentialsPolicy={tryItCredentialsPolicy} tryItCorsProxy={tryItCorsProxy} renderExtensionAddon={renderExtensionAddon} + basePath={basePath} /> )} {layout === 'responsive' && ( @@ -254,6 +256,7 @@ export const APIImpl: React.FC = props => { tryItCorsProxy={tryItCorsProxy} renderExtensionAddon={renderExtensionAddon} compact={isResponsiveLayoutEnabled} + basePath={basePath} /> )}