fix(techdocs): prevent to throw an error when the SCM provider is not supported (#29154)

* fix(techdocs): prevent to throw error on undefined 'selection'

Error
TypeError

Message
Failed to execute 'containsNode' on 'Selection': parameter 1 is not of type 'Node'.

Stack Trace
TypeError: parameter 1 is not of type 'Node'.
    at https://backstage.mpi-internal.com/static/module-backstage.b2517ed5.js:618:1729
    at ul (https://backstage.mpi-internal.com/static/module-react-dom.b4fe5cf3.js:16:24313)
    at Ct (https://backstage.mpi-internal.com/static/module-react-dom.b4fe5cf3.js:16:42455)
    at qs (https://backstage.mpi-internal.com/static/module-react-dom.b4fe5cf3.js:16:34578)
    at Ln (https://backstage.mpi-internal.com/static/module-react-dom.b4fe5cf3.js:24:1590)
    at MessagePort.Zn (https://backstage.mpi-internal.com/static/module-react-dom.b4fe5cf3.js:24:1980)

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>

* doc: add changeset

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>

* chore(techinsights-addon): doesn't load the component for unsuported repositories

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>

* Update plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssueContent.tsx

Co-authored-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
Signed-off-by: Andy Ladjadj <andy.ladjadj@adevinta.com>

* Update plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssueContent.tsx

Co-authored-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
Signed-off-by: Andy Ladjadj <andy.ladjadj@adevinta.com>

* fix: doesn't test that selection contains feebackContainer

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>

* ci: update the changeset

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>

---------

Signed-off-by: Andy LADJADJ <andy.ladjadj@adevinta.com>
Signed-off-by: Andy Ladjadj <andy.ladjadj@adevinta.com>
Co-authored-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
This commit is contained in:
Andy Ladjadj
2025-03-27 00:40:26 +01:00
committed by GitHub
parent a31a430c17
commit 9c12a76c9e
6 changed files with 203 additions and 123 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-module-addons-contrib': patch
---
Fixed rendering issues in `ReportIssue` addon for unsupported repository types and improved shadow DOM event handling. The addon now properly prevents initialization when encountering unsupported repository types and correctly handles selection events within the shadow DOM.
@@ -48,10 +48,7 @@ const getIcon = ({ type }: Repository) => {
};
const getName = ({ type }: Repository) => {
if (type === 'github') {
return 'Github';
}
return 'Gitlab';
return type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1);
};
const getUrl = (repository: Repository, template: ReportIssueTemplate) => {
@@ -116,7 +116,7 @@ describe('ReportIssue', () => {
it('renders gitlab link without exploding', async () => {
byUrl.mockReturnValue({ type: 'gitlab' });
const { shadowRoot, getByText } =
const { shadowRoot, getByText, queryByTestId } =
await TechDocsAddonTester.buildAddonsInTechDocs([
<ReportIssue debounceTime={0} />,
])
@@ -164,6 +164,8 @@ describe('ReportIssue', () => {
fireSelectionChangeEvent(window);
await waitFor(() => {
expect(queryByTestId('report-issue-addon')).toBeInTheDocument();
const link = getByText('Open new Gitlab issue');
expect(link).toHaveAttribute(
'href',
@@ -180,7 +182,7 @@ describe('ReportIssue', () => {
body: options.selection.toString().trim(),
});
const { shadowRoot, getByText } =
const { shadowRoot, getByText, queryByTestId } =
await TechDocsAddonTester.buildAddonsInTechDocs([
<ReportIssue debounceTime={0} templateBuilder={templateBuilder} />,
])
@@ -228,6 +230,8 @@ describe('ReportIssue', () => {
fireSelectionChangeEvent(window);
await waitFor(() => {
expect(queryByTestId('report-issue-addon')).toBeInTheDocument();
const link = getByText('Open new Gitlab issue');
expect(link).toHaveAttribute(
'href',
@@ -235,4 +239,49 @@ describe('ReportIssue', () => {
);
});
});
it('does not render report issue link for unsupported repository type', async () => {
byUrl.mockReturnValue({ type: 'gerrit', resource: 'gerrit.example.com' });
const { shadowRoot, getByText, queryByTestId } =
await TechDocsAddonTester.buildAddonsInTechDocs([
<ReportIssue debounceTime={0} />,
])
.withDom(
<html lang="en">
<head />
<body>
<div data-md-component="content">
<div data-md-component="main">
<div className="md-content">
<article>
<a
title="Edit this page"
href="https://gerrit.example.com/admin/repos/edit/repo/my/repo/branch/refs/heads/master/file/docs/README.md"
>
Edit page
</a>
</article>
</div>
</div>
</div>
</body>
</html>,
)
.withApis([[scmIntegrationsApiRef, { byUrl }]])
.renderWithEffects();
(shadowRoot as ShadowRoot & Pick<Document, 'getSelection'>).getSelection =
() => selection;
await waitFor(() => {
expect(getByText('Edit page')).toBeInTheDocument();
});
fireSelectionChangeEvent(window);
await waitFor(() => {
expect(queryByTestId('report-issue-addon')).not.toBeInTheDocument();
});
});
});
@@ -14,39 +14,11 @@
* limitations under the License.
*/
import React, { useState, useEffect } from 'react';
import { makeStyles, Portal, Paper } from '@material-ui/core';
import { useGitTemplate, useGitRepository } from './hooks';
import React from 'react';
import { useGitRepository } from './hooks';
import { ReportIssueTemplateBuilder } from './types';
import {
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
ADDON_FEEDBACK_CONTAINER_ID,
ADDON_FEEDBACK_CONTAINER_SELECTOR,
} from './constants';
import { IssueLink } from './IssueLink';
import {
useShadowRootElements,
useShadowRootSelection,
} from '@backstage/plugin-techdocs-react';
const useStyles = makeStyles(theme => ({
root: {
transform: 'translate(-100%, -100%)',
position: 'absolute',
padding: theme.spacing(1),
zIndex: theme.zIndex.tooltip,
background: theme.palette.common.white,
},
}));
type Style = {
top: string;
left: string;
};
import { ADDON_ISSUE_REPO_TYPES_SUPPORTED } from './constants';
import { ReportIssueAddonContent } from './ReportIssueContent';
/**
* Props customizing the <ReportIssue /> Addon.
@@ -67,101 +39,24 @@ export type ReportIssueProps = {
templateBuilder?: ReportIssueTemplateBuilder;
};
/**
* Show report issue button when text is highlighted
*/
export const ReportIssueAddon = ({
debounceTime = 500,
templateBuilder: buildTemplate,
templateBuilder,
}: ReportIssueProps) => {
const classes = useStyles();
const [style, setStyle] = useState<Style>();
const repository = useGitRepository();
const defaultTemplate = useGitTemplate(debounceTime);
const selection = useShadowRootSelection(debounceTime);
const [mainContent, feedbackLink] = useShadowRootElements([
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
]);
let [feedbackContainer] = useShadowRootElements([
ADDON_FEEDBACK_CONTAINER_SELECTOR,
]);
if (feedbackLink) {
feedbackLink.style.display = 'none';
}
// calculates the position of the selected text to be able to set the position of the addon
useEffect(() => {
if (
// todo(backstage/techdocs-core) handle non-repo rendering
!repository ||
!selection ||
!selection.containsNode(mainContent!, true) ||
selection?.containsNode(feedbackContainer!, true)
) {
return;
}
const mainContentPosition = mainContent!.getBoundingClientRect();
const selectionPosition = selection.getRangeAt(0).getBoundingClientRect();
// Calculating the distance between the selection's top and the main content's top
const distanceFromTop = selectionPosition.top - mainContentPosition.top;
const minDistanceFromTop = 50;
// Defining a base value for 'top'
let top = distanceFromTop < minDistanceFromTop ? 101 : distanceFromTop - 16;
// Checking if the main content is off-screen towards the top
if (mainContentPosition.top < 0) {
const absMainContentTop = Math.abs(mainContentPosition.top);
// Adjusting 'top' if the selection is close to the top edge and the main content is off-screen
if (distanceFromTop - absMainContentTop < minDistanceFromTop) {
top += 89;
}
}
setStyle({
top: `${top}px`,
left: `${selectionPosition.left + selectionPosition.width / 2}px`,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection, mainContent, feedbackContainer]);
if (
!selection ||
!repository ||
!['github', 'gitlab'].includes(repository.type)
)
!ADDON_ISSUE_REPO_TYPES_SUPPORTED.includes(repository.type)
) {
return null;
if (!feedbackContainer) {
feedbackContainer = document.createElement('div');
feedbackContainer.setAttribute('id', ADDON_FEEDBACK_CONTAINER_ID);
mainContent!.prepend(feedbackContainer);
}
return (
<Portal container={feedbackContainer}>
<Paper
data-testid="report-issue-addon"
className={classes.root}
style={style}
>
<IssueLink
repository={repository}
template={
buildTemplate ? buildTemplate({ selection }) : defaultTemplate
}
/>
</Paper>
</Portal>
<ReportIssueAddonContent
debounceTime={debounceTime}
templateBuilder={templateBuilder}
repository={repository}
/>
);
};
@@ -0,0 +1,133 @@
/*
* Copyright 2022 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 React, { useState, useEffect } from 'react';
import { makeStyles, Portal, Paper } from '@material-ui/core';
import { useGitTemplate } from './hooks';
import {
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
ADDON_FEEDBACK_CONTAINER_ID,
ADDON_FEEDBACK_CONTAINER_SELECTOR,
} from './constants';
import { IssueLink } from './IssueLink';
import {
useShadowRootElements,
useShadowRootSelection,
} from '@backstage/plugin-techdocs-react';
import { Repository } from './types';
import { ReportIssueProps } from './ReportIssue';
const useStyles = makeStyles(theme => ({
root: {
transform: 'translate(-100%, -100%)',
position: 'absolute',
padding: theme.spacing(1),
zIndex: theme.zIndex.tooltip,
background: theme.palette.common.white,
},
}));
type Style = {
top: string;
left: string;
};
// Props customizing the <ReportIssue /> Addon.
export type ReportIssueAddonContentProps = ReportIssueProps & {
repository: Repository;
};
export const ReportIssueAddonContent = ({
debounceTime,
templateBuilder,
repository,
}: ReportIssueAddonContentProps) => {
const classes = useStyles();
const [style, setStyle] = useState<Style>();
const defaultTemplate = useGitTemplate(debounceTime);
const selection = useShadowRootSelection(debounceTime);
const [mainContent, feedbackLink] = useShadowRootElements([
PAGE_MAIN_CONTENT_SELECTOR,
PAGE_FEEDBACK_LINK_SELECTOR,
]);
let [feedbackContainer] = useShadowRootElements([
ADDON_FEEDBACK_CONTAINER_SELECTOR,
]);
if (feedbackLink) {
feedbackLink.style.display = 'none';
}
useEffect(() => {
if (
!selection ||
!selection.containsNode(mainContent, true) ||
selection.containsNode(feedbackContainer, true)
) {
return;
}
const mainContentPosition = mainContent.getBoundingClientRect();
const selectionPosition = selection.getRangeAt(0).getBoundingClientRect();
const distanceFromTop = selectionPosition.top - mainContentPosition.top;
const minDistanceFromTop = 50;
let top = distanceFromTop < minDistanceFromTop ? 101 : distanceFromTop - 16;
if (mainContentPosition.top < 0) {
const absMainContentTop = Math.abs(mainContentPosition.top);
if (distanceFromTop - absMainContentTop < minDistanceFromTop) {
top += 89;
}
}
setStyle({
top: `${top}px`,
left: `${selectionPosition.left + selectionPosition.width / 2}px`,
});
}, [selection, mainContent, feedbackContainer]);
if (!feedbackContainer) {
feedbackContainer = document.createElement('div');
feedbackContainer.setAttribute('id', ADDON_FEEDBACK_CONTAINER_ID);
mainContent!.prepend(feedbackContainer);
}
if (!selection || !selection.containsNode(mainContent, true)) {
return null;
}
return (
<Portal container={feedbackContainer}>
<Paper
data-testid="report-issue-addon"
className={classes.root}
style={style}
>
<IssueLink
repository={repository}
template={
templateBuilder ? templateBuilder({ selection }) : defaultTemplate
}
/>
</Paper>
</Portal>
);
};
@@ -16,6 +16,7 @@
export const ADDON_FEEDBACK_CONTAINER_ID = 'techdocs-report-issue';
export const ADDON_FEEDBACK_CONTAINER_SELECTOR = `#${ADDON_FEEDBACK_CONTAINER_ID}`;
export const ADDON_ISSUE_REPO_TYPES_SUPPORTED = ['github', 'gitlab'];
export const PAGE_EDIT_LINK_SELECTOR = '[title^="Edit this page"]';
export const PAGE_FEEDBACK_LINK_SELECTOR = '[title^="Leave feedback for"]';
export const PAGE_MAIN_CONTENT_SELECTOR =