Merge remote-tracking branch 'backstage/master' into fix/14026
This commit is contained in:
@@ -1,5 +1,87 @@
|
||||
# @backstage/plugin-techdocs
|
||||
|
||||
## 1.4.0-next.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5691baea69: Add ability to configure filters when using EntityListDocsGrid
|
||||
|
||||
The following example will render two sections of cards grid:
|
||||
|
||||
- One section for documentations tagged as `recommended`
|
||||
- One section for documentations tagged as `runbook`
|
||||
|
||||
```js
|
||||
<EntityListDocsGrid groups={{[
|
||||
{
|
||||
title: "Recommended Documentation",
|
||||
filterPredicate: entity =>
|
||||
entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
},
|
||||
{
|
||||
title: "RunBooks Documentation",
|
||||
filterPredicate: entity =>
|
||||
entity?.metadata?.tags?.includes('runbook') ?? false,
|
||||
}
|
||||
]}} />
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cbe11d1e23: Tweak README
|
||||
- 7573b65232: Internal refactor of imports to avoid circular dependencies
|
||||
- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions.
|
||||
- 3a1a999b7b: Include query parameters when navigating to relative links in documents
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-react@1.2.1-next.0
|
||||
- @backstage/core-components@0.12.0-next.0
|
||||
- @backstage/core-plugin-api@1.1.0-next.0
|
||||
- @backstage/integration@1.4.0-next.0
|
||||
- @backstage/catalog-model@1.1.3-next.0
|
||||
- @backstage/integration-react@1.1.6-next.0
|
||||
- @backstage/plugin-search-react@1.2.1-next.0
|
||||
- @backstage/plugin-techdocs-react@1.0.6-next.0
|
||||
- @backstage/config@1.0.4-next.0
|
||||
- @backstage/errors@1.1.3-next.0
|
||||
- @backstage/theme@0.2.16
|
||||
- @backstage/plugin-search-common@1.1.1-next.0
|
||||
|
||||
## 1.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/catalog-model@1.1.2
|
||||
- @backstage/plugin-catalog-react@1.2.0
|
||||
- @backstage/core-components@0.11.2
|
||||
- @backstage/plugin-search-react@1.2.0
|
||||
- @backstage/plugin-search-common@1.1.0
|
||||
- @backstage/plugin-techdocs-react@1.0.5
|
||||
- @backstage/integration-react@1.1.5
|
||||
- @backstage/core-plugin-api@1.0.7
|
||||
- @backstage/config@1.0.3
|
||||
- @backstage/errors@1.1.2
|
||||
- @backstage/integration@1.3.2
|
||||
- @backstage/theme@0.2.16
|
||||
|
||||
## 1.3.3-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-react@1.2.0-next.2
|
||||
- @backstage/plugin-search-common@1.1.0-next.2
|
||||
- @backstage/catalog-model@1.1.2-next.2
|
||||
- @backstage/config@1.0.3-next.2
|
||||
- @backstage/core-components@0.11.2-next.2
|
||||
- @backstage/core-plugin-api@1.0.7-next.2
|
||||
- @backstage/errors@1.1.2-next.2
|
||||
- @backstage/integration@1.3.2-next.2
|
||||
- @backstage/integration-react@1.1.5-next.2
|
||||
- @backstage/theme@0.2.16
|
||||
- @backstage/plugin-search-react@1.2.0-next.2
|
||||
- @backstage/plugin-techdocs-react@1.0.5-next.2
|
||||
|
||||
## 1.3.3-next.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -59,6 +59,12 @@ export type DocsCardGridProps = {
|
||||
entities: Entity[] | undefined;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type DocsGroupConfig = {
|
||||
title: React_2.ReactNode;
|
||||
filterPredicate: ((entity: Entity) => boolean) | string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export const DocsTable: {
|
||||
(props: DocsTableProps): JSX.Element | null;
|
||||
@@ -112,7 +118,14 @@ export const EmbeddedDocsRouter: (
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @public
|
||||
export const EntityListDocsGrid: () => JSX.Element;
|
||||
export const EntityListDocsGrid: ({
|
||||
groups,
|
||||
}: EntityListDocsGridPageProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityListDocsGridPageProps = {
|
||||
groups?: DocsGroupConfig[];
|
||||
};
|
||||
|
||||
// @public
|
||||
export const EntityListDocsTable: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@backstage/plugin-techdocs",
|
||||
"description": "The Backstage plugin that renders technical documentation for your components",
|
||||
"version": "1.3.3-next.1",
|
||||
"version": "1.4.0-next.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
|
||||
import {
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
storageApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
CatalogApi,
|
||||
catalogApiRef,
|
||||
starredEntitiesApiRef,
|
||||
MockEntityListContextProvider,
|
||||
MockStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
MockStorageApi,
|
||||
renderInTestApp,
|
||||
TestApiRegistry,
|
||||
} from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { rootDocsRouteRef } from '../../../routes';
|
||||
import { EntityListDocsGrid } from './EntityListDocsGrid';
|
||||
|
||||
const entities = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Documentation #1',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'Documentation #2',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
type: 'documentation',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockCatalogApi = {
|
||||
getEntityByRef: () => Promise.resolve(),
|
||||
getEntities: async () => ({
|
||||
items: entities,
|
||||
}),
|
||||
} as Partial<CatalogApi>;
|
||||
|
||||
describe('Entity List Docs Grid', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const configApi: ConfigApi = new ConfigReader({
|
||||
organization: {
|
||||
name: 'My Company',
|
||||
},
|
||||
});
|
||||
|
||||
const storageApi = MockStorageApi.create();
|
||||
|
||||
const apiRegistry = TestApiRegistry.from(
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[configApiRef, configApi],
|
||||
[storageApiRef, storageApi],
|
||||
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
|
||||
);
|
||||
|
||||
it('should render all entitites without filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.queryByText('All Documentation')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #1')).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #2')).toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render only filtered entities with filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid
|
||||
groups={[
|
||||
{
|
||||
title: 'Curated Documentation',
|
||||
filterPredicate: entity =>
|
||||
entity.metadata.name === 'Documentation #1',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('Curated Documentation'),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.queryByText('Documentation #1')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render nothing with filtering yielding no result', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: entities }}>
|
||||
<EntityListDocsGrid
|
||||
groups={[
|
||||
{
|
||||
title: 'Curated Documentation',
|
||||
filterPredicate: entity =>
|
||||
entity.metadata.name === 'Documentation #3',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('Curated Documentation'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #1'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render an error without any documentation and without filtering', async () => {
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<MockEntityListContextProvider value={{ entities: [] }}>
|
||||
<EntityListDocsGrid />
|
||||
</MockEntityListContextProvider>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.queryByText('All Documentation'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #1'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText('Documentation #2'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(await screen.queryByTestId('doc-not-found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -15,20 +15,95 @@
|
||||
*/
|
||||
|
||||
import { DocsCardGrid } from './DocsCardGrid';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
Content,
|
||||
ContentHeader,
|
||||
Link,
|
||||
Progress,
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { useEntityList } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
useEntityList,
|
||||
useEntityOwnership,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityListDocsGrid}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type DocsGroupConfig = {
|
||||
title: React.ReactNode;
|
||||
filterPredicate: ((entity: Entity) => boolean) | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for {@link EntityListDocsGrid}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityListDocsGridPageProps = {
|
||||
groups?: DocsGroupConfig[];
|
||||
};
|
||||
|
||||
const allEntitiesGroup: DocsGroupConfig = {
|
||||
title: 'All Documentation',
|
||||
filterPredicate: () => true,
|
||||
};
|
||||
|
||||
const EntityListDocsGridGroup = ({
|
||||
entities,
|
||||
group,
|
||||
}: {
|
||||
group: DocsGroupConfig;
|
||||
entities: Entity[];
|
||||
}) => {
|
||||
const { loading: loadingOwnership, isOwnedEntity } = useEntityOwnership();
|
||||
|
||||
const shownEntities = entities.filter(entity => {
|
||||
if (group.filterPredicate === 'ownedByUser') {
|
||||
if (loadingOwnership) {
|
||||
return false;
|
||||
}
|
||||
return isOwnedEntity(entity);
|
||||
}
|
||||
|
||||
return (
|
||||
typeof group.filterPredicate === 'function' &&
|
||||
group.filterPredicate(entity)
|
||||
);
|
||||
});
|
||||
|
||||
const titleComponent: React.ReactNode = (() => {
|
||||
return typeof group.title === 'string' ? (
|
||||
<ContentHeader title={group.title} />
|
||||
) : (
|
||||
group.title
|
||||
);
|
||||
})();
|
||||
|
||||
if (shownEntities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
{titleComponent}
|
||||
<DocsCardGrid entities={shownEntities} />
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Component responsible to get entities from entity list context and pass down to DocsCardGrid
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const EntityListDocsGrid = () => {
|
||||
export const EntityListDocsGrid = ({ groups }: EntityListDocsGridPageProps) => {
|
||||
const { loading, error, entities } = useEntityList();
|
||||
|
||||
if (error) {
|
||||
@@ -42,15 +117,39 @@ export const EntityListDocsGrid = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !entities) {
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (entities.length === 0) {
|
||||
return (
|
||||
<div data-testid="doc-not-found">
|
||||
<Typography variant="body2">
|
||||
No documentation found that match your filter. Learn more about{' '}
|
||||
<Link to="https://backstage.io/docs/features/techdocs/creating-and-publishing">
|
||||
publishing documentation
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
entities.sort((a, b) =>
|
||||
(a.metadata.title ?? a.metadata.name).localeCompare(
|
||||
b.metadata.title ?? b.metadata.name,
|
||||
),
|
||||
);
|
||||
|
||||
return <DocsCardGrid entities={entities} />;
|
||||
return (
|
||||
<Content>
|
||||
{(groups || [allEntitiesGroup]).map((group, index: number) => (
|
||||
<EntityListDocsGridGroup
|
||||
entities={entities}
|
||||
group={group}
|
||||
key={`${group.title}-${index}`}
|
||||
/>
|
||||
))}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,4 +17,4 @@ export const updateH1Text = (): Transformer => {
|
||||
};
|
||||
```
|
||||
|
||||
The transformers are then registered in the Reader.tsx file. They are registered in two places, one place that runs before it's attached to the actual browser DOM (preTransformers) and once after (postTransfomers). Doing modifications is faster before it's attached, but doesn't allow us to do some things, such as attaching event listeners.
|
||||
The transformers are then registered in the Reader.tsx file. They are registered in two places, one place that runs before it's attached to the actual browser DOM (`preTransformer`s) and once after (`postTransformer`s). Doing modifications is faster before it's attached, but doesn't allow us to do some things, such as attaching event listeners.
|
||||
|
||||
@@ -35,6 +35,82 @@ import {
|
||||
useRouteRefParams,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
/* An explanation for the multiple ways of customizing the TechDocs reader page
|
||||
|
||||
Please refer to this page on the microsite for the latest recommended approach:
|
||||
https://backstage.io/docs/features/techdocs/how-to-guides#how-to-customize-the-techdocs-reader-page
|
||||
|
||||
The <TechDocsReaderPage> component is responsible for rendering the <TechDocsReaderPageProvider> and
|
||||
its contained version of a <Page>, which in turn renders the <TechDocsReaderPageContent>.
|
||||
|
||||
Historically, there have been different approaches on how this <Page> can be customized, and how the
|
||||
<TechDocsReaderPageContent> inside could be exchanged for a custom implementation (which was not
|
||||
possible before). Also, the current implementation supports every scenario to avoid breaking default
|
||||
configurations of TechDocs.
|
||||
|
||||
In particular, there are 4 different TechDocs page configurations:
|
||||
|
||||
CONFIGURATION 1: <TechDocsReaderPage> only, no children
|
||||
|
||||
<Route path="/docs/:namespace/:kind/:name/*" element={<TechDocsReaderPage />} >
|
||||
|
||||
This is the simplest way to use TechDocs. Only a full page is passed, assuming that it comes with
|
||||
its content inside. Since we allowed customizing it, we started providing <TechDocsReaderLayout> as
|
||||
a default implementation (which contains <TechDocsReaderPageContent>).
|
||||
|
||||
CONFIGURATION 2 (not advised): <TechDocsReaderPage> with element children
|
||||
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={
|
||||
<TechDocsReaderPage>
|
||||
{techdocsPage}
|
||||
</TechDocsReaderPage>
|
||||
}
|
||||
/>
|
||||
|
||||
Previously, there were two ways of passing children to <TechDocsReaderPage>: either as elements (as
|
||||
shown above), or as a render function (described below in CONFIGURATION 3). The "techdocsPage" is
|
||||
located in packages/app/src/components/techdocs and is the default implementation of the content
|
||||
inside.
|
||||
|
||||
CONFIGURATION 3 (not advised): <TechDocsReaderPage> with render function as child
|
||||
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={
|
||||
<TechDocsReaderPage>
|
||||
{({ metadata, entityMetadata, onReady }) => (
|
||||
techdocsPage
|
||||
)}
|
||||
</TechDocsReaderPage>
|
||||
}
|
||||
/>
|
||||
|
||||
Similar to CONFIGURATION 2, the direct children will be passed to the <TechDocsReaderPage> but in
|
||||
this case interpreted as render prop.
|
||||
|
||||
CONFIGURATION 4: <TechDocsReaderPage> and provided content in <Route>
|
||||
|
||||
<Route
|
||||
path="/docs/:namespace/:kind/:name/*"
|
||||
element={<TechDocsReaderPage />}
|
||||
>
|
||||
{techDocsPage}
|
||||
<TechDocsAddons>
|
||||
<ExpandableNavigation />
|
||||
<ReportIssue />
|
||||
<TextSize />
|
||||
</TechDocsAddons>
|
||||
</Route>
|
||||
|
||||
This is the current state in packages/app/src/App.tsx and moved the location of children from inside
|
||||
the element prop in the <Route> to the children of the <Route>. Then, in <TechDocsReaderPage> they
|
||||
are retrieved using the useOutlet hook from React Router.
|
||||
|
||||
NOTE: Render functions are no longer supported in this approach.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Props for {@link TechDocsReaderLayout}
|
||||
* @public
|
||||
@@ -77,6 +153,7 @@ export type TechDocsReaderPageProps = {
|
||||
|
||||
/**
|
||||
* An addon-aware implementation of the TechDocsReaderPage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
@@ -88,21 +165,14 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
if (!children) {
|
||||
const childrenList = outlet ? Children.toArray(outlet.props.children) : [];
|
||||
|
||||
const page = childrenList.find(child => {
|
||||
if (getComponentData(child, TECHDOCS_ADDONS_WRAPPER_KEY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// react-router 6 stable wraps children in a routing context provider, so check one level deeper
|
||||
const nestedChildren = (child as ReactElement)?.props?.children;
|
||||
if (nestedChildren) {
|
||||
return !Children.toArray(nestedChildren).some(nested =>
|
||||
getComponentData(nested, TECHDOCS_ADDONS_WRAPPER_KEY),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const grandChildren = childrenList.flatMap(
|
||||
child => (child as ReactElement)?.props?.children ?? [],
|
||||
);
|
||||
const page: React.ReactNode = grandChildren.find(
|
||||
grandChild => !getComponentData(grandChild, TECHDOCS_ADDONS_WRAPPER_KEY),
|
||||
);
|
||||
|
||||
// As explained above, "page" is configuration 4 and <TechDocsReaderLayout> is 1
|
||||
return (
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{(page as JSX.Element) || <TechDocsReaderLayout />}
|
||||
@@ -110,6 +180,7 @@ export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// As explained above, a render function is configuration 3 and React element is 2
|
||||
return (
|
||||
<TechDocsReaderPageProvider entityRef={entityRef}>
|
||||
{({ metadata, entityMetadata, onReady }) => (
|
||||
|
||||
@@ -175,13 +175,14 @@ export const useTechDocsReaderDom = (
|
||||
// detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
|
||||
const modifierActive = event.ctrlKey || event.metaKey;
|
||||
const parsedUrl = new URL(url);
|
||||
const fullPath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
||||
|
||||
// hash exists when anchor is clicked on secondary sidebar
|
||||
if (parsedUrl.hash) {
|
||||
if (modifierActive) {
|
||||
window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
|
||||
window.open(fullPath, '_blank');
|
||||
} else {
|
||||
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
|
||||
navigate(fullPath);
|
||||
// Scroll to hash if it's on the current page
|
||||
transformedElement
|
||||
?.querySelector(`[id="${parsedUrl.hash.slice(1)}"]`)
|
||||
@@ -189,9 +190,9 @@ export const useTechDocsReaderDom = (
|
||||
}
|
||||
} else {
|
||||
if (modifierActive) {
|
||||
window.open(parsedUrl.pathname, '_blank');
|
||||
window.open(fullPath, '_blank');
|
||||
} else {
|
||||
navigate(parsedUrl.pathname);
|
||||
navigate(fullPath);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Transformer } from './index';
|
||||
import type { Transformer } from './transformer';
|
||||
import {
|
||||
replaceGitHubUrlType,
|
||||
replaceGithubUrlType,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import FeedbackOutlinedIcon from '@material-ui/icons/FeedbackOutlined';
|
||||
@@ -59,7 +59,7 @@ export const addGitFeedbackLink = (
|
||||
// Convert GitHub edit url to blob type so it can be parsed by git-url-parse correctly
|
||||
const gitUrl =
|
||||
integration?.type === 'github'
|
||||
? replaceGitHubUrlType(sourceURL.href, 'blob')
|
||||
? replaceGithubUrlType(sourceURL.href, 'blob')
|
||||
: sourceURL.href;
|
||||
const gitInfo = parseGitUrl(gitUrl);
|
||||
const repoPath = `/${gitInfo.organization}/${gitInfo.name}`;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Transformer } from './index';
|
||||
import type { Transformer } from './transformer';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { scrollIntoNavigation } from '.';
|
||||
import { scrollIntoNavigation } from './scrollIntoNavigation';
|
||||
import { createTestShadowDom, FIXTURES } from '../../test-utils';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
Reference in New Issue
Block a user