diff --git a/.changeset/techdocs-five-hundred-ml.md b/.changeset/techdocs-five-hundred-ml.md new file mode 100644 index 0000000000..775a5c55ab --- /dev/null +++ b/.changeset/techdocs-five-hundred-ml.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': patch +--- + +The TechDocs CLI's embedded app now imports all API refs from the `@backstage/plugin-techdocs-react` package. diff --git a/.changeset/techdocs-low-calorie-drink.md b/.changeset/techdocs-low-calorie-drink.md new file mode 100644 index 0000000000..6290085d91 --- /dev/null +++ b/.changeset/techdocs-low-calorie-drink.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-react': patch +'@backstage/plugin-techdocs': patch +--- + +The `TechDocsStorageApi` and its associated ref are now exported by `@backstage/plugin-techdocs-react`. The API interface, ref, and types are now deprecated in `@backstage/plugin-techdocs` and will be removed in a future release. diff --git a/.changeset/techdocs-vitamin-well-reload.md b/.changeset/techdocs-vitamin-well-reload.md new file mode 100644 index 0000000000..967cc6b6a6 --- /dev/null +++ b/.changeset/techdocs-vitamin-well-reload.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-addons-test-utils': minor +--- + +Introducing a package with utilities to help test TechDocs Addons. diff --git a/packages/techdocs-cli-embedded-app/src/apis.ts b/packages/techdocs-cli-embedded-app/src/apis.ts index d230a9f83c..7156da902f 100644 --- a/packages/techdocs-cli-embedded-app/src/apis.ts +++ b/packages/techdocs-cli-embedded-app/src/apis.ts @@ -35,7 +35,7 @@ import { techdocsApiRef, TechDocsStorageApi, techdocsStorageApiRef, -} from '@backstage/plugin-techdocs'; +} from '@backstage/plugin-techdocs-react'; // TODO: Export type from plugin-techdocs and import this here // import { ParsedEntityId } from '@backstage/plugin-techdocs' diff --git a/plugins/techdocs-addons-test-utils/.eslintrc.js b/plugins/techdocs-addons-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/techdocs-addons-test-utils/README.md b/plugins/techdocs-addons-test-utils/README.md new file mode 100644 index 0000000000..a6ac2e3182 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/README.md @@ -0,0 +1,3 @@ +# techdocs-addons-test-utils + +A package containing utilities for testing TechDocs Addons. diff --git a/plugins/techdocs-addons-test-utils/api-report.md b/plugins/techdocs-addons-test-utils/api-report.md new file mode 100644 index 0000000000..a32d2d4854 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/api-report.md @@ -0,0 +1,39 @@ +## API Report File for "@backstage/plugin-techdocs-addons-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ApiRef } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; +import { ReactElement } from 'react'; +import { screen as screen_2 } from 'testing-library__dom'; +import { TechDocsEntityMetadata } from '@backstage/plugin-techdocs-react'; +import { TechDocsMetadata } from '@backstage/plugin-techdocs-react'; + +// @public (undocumented) +export class TechDocsAddonTester { + // (undocumented) + atPath(path: string): this; + // (undocumented) + build(): React_2.ReactElement< + any, + string | React_2.JSXElementConstructor + >; + // (undocumented) + static buildAddonsInTechDocs(addons: ReactElement[]): TechDocsAddonTester; + // (undocumented) + renderWithEffects(): Promise< + typeof screen_2 & { + shadowRoot: ShadowRoot | null; + } + >; + // (undocumented) + withApis(apis: TechdocsAddonTesterApis): this; + // (undocumented) + withDom(dom: ReactElement): this; + // (undocumented) + withEntity(entity: Partial): this; + // (undocumented) + withMetadata(metadata: Partial): this; +} +``` diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json new file mode 100644 index 0000000000..c1240ae5ca --- /dev/null +++ b/plugins/techdocs-addons-test-utils/package.json @@ -0,0 +1,72 @@ +{ + "name": "@backstage/plugin-techdocs-addons-test-utils", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/techdocs-addons-test-utils" + }, + "keywords": [ + "backstage", + "techdocs" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "^0.9.3", + "@backstage/core-app-api": "^1.0.1", + "@backstage/core-plugin-api": "^1.0.1", + "@backstage/integration-react": "^1.1.0-next.0", + "@backstage/plugin-catalog": "^1.2.0-next.0", + "@backstage/plugin-search-react": "^0.1.1-next.0", + "@backstage/plugin-techdocs": "^1.1.1-next.0", + "@backstage/plugin-techdocs-react": "^0.1.1-next.0", + "@backstage/test-utils": "^1.0.2-next.0", + "@backstage/theme": "^0.2.15", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4", + "react-router-dom": "6.0.0-beta.0", + "@testing-library/react": "^12.1.3", + "testing-library__dom": "^7.29.4-beta.1" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-dom": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.17.1-next.0", + "@backstage/dev-utils": "^1.0.2-next.0", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/jest": "^26.0.7", + "@types/node": "^16.11.26", + "msw": "^0.35.0", + "cross-fetch": "^3.1.5" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/techdocs-addons-test-utils/src/index.ts b/plugins/techdocs-addons-test-utils/src/index.ts new file mode 100644 index 0000000000..2e4348fe2a --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * A package containing test utilities for TechDocs Addons. + * + * @packageDocumentation + */ + +export * from './test-utils'; diff --git a/plugins/techdocs-addons-test-utils/src/setupTests.ts b/plugins/techdocs-addons-test-utils/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/techdocs-addons-test-utils/src/test-utils.tsx b/plugins/techdocs-addons-test-utils/src/test-utils.tsx new file mode 100644 index 0000000000..7894b96154 --- /dev/null +++ b/plugins/techdocs-addons-test-utils/src/test-utils.tsx @@ -0,0 +1,239 @@ +/* + * 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, { ReactElement, Fragment } from 'react'; + +// Shadow DOM support for the simple and complete DOM testing utilities +// https://github.com/testing-library/dom-testing-library/issues/742#issuecomment-674987855 +import { screen } from 'testing-library__dom'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Route } from 'react-router-dom'; +import { act, render } from '@testing-library/react'; + +import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { FlatRoutes } from '@backstage/core-app-api'; +import { ApiRef } from '@backstage/core-plugin-api'; + +import { + TechDocsAddons, + techdocsApiRef, + TechDocsEntityMetadata, + TechDocsMetadata, + techdocsStorageApiRef, +} from '@backstage/plugin-techdocs-react'; +import { TechDocsReaderPage, techdocsPlugin } from '@backstage/plugin-techdocs'; +import { catalogPlugin } from '@backstage/plugin-catalog'; +import { searchApiRef } from '@backstage/plugin-search-react'; +import { scmIntegrationsApiRef } from '@backstage/integration-react'; + +const techdocsApi = { + getTechDocsMetadata: jest.fn(), + getEntityMetadata: jest.fn(), +}; + +const techdocsStorageApi = { + getApiOrigin: jest.fn(), + getEntityDocs: jest.fn(), + syncEntityDocs: jest.fn(), +}; + +const searchApi = { + query: jest.fn().mockResolvedValue({ results: [] }), +}; + +const scmIntegrationsApi = { + fromConfig: jest.fn().mockReturnValue({}), +}; + +/** @ignore */ +type TechDocsAddonTesterTestApiPair = TApi extends infer TImpl + ? readonly [ApiRef, Partial] + : never; + +/** @ignore */ +type TechdocsAddonTesterApis = TechDocsAddonTesterTestApiPair[]; + +type TechDocsAddonTesterOptions = { + dom: ReactElement; + entity: Partial; + metadata: Partial; + componentId: string; + apis: TechdocsAddonTesterApis; + path: string; +}; + +const defaultOptions: TechDocsAddonTesterOptions = { + dom: <>, + entity: {}, + metadata: {}, + componentId: 'docs', + apis: [], + path: '', +}; + +const defaultMetadata = { + site_name: 'Tech Docs', + site_description: 'Tech Docs', +}; + +const defaultEntity = { + kind: 'Component', + metadata: { namespace: 'default', name: 'docs' }, +}; + +const defaultDom = ( + + + +
+
+
+
+
+ + +); + +/** + * @public + */ + +export class TechDocsAddonTester { + private options: TechDocsAddonTesterOptions = defaultOptions; + private addons: ReactElement[]; + + static buildAddonsInTechDocs(addons: ReactElement[]) { + return new TechDocsAddonTester(addons); + } + + private constructor(addons: ReactElement[]) { + this.addons = addons; + } + + withApis(apis: TechdocsAddonTesterApis) { + const refs = apis.map(([ref]) => ref); + this.options.apis = this.options.apis + .filter(([ref]) => !refs.includes(ref)) + .concat(apis); + return this; + } + + withDom(dom: ReactElement) { + this.options.dom = dom; + return this; + } + + withMetadata(metadata: Partial) { + this.options.metadata = metadata; + return this; + } + + withEntity(entity: Partial) { + this.options.entity = entity; + return this; + } + + atPath(path: string) { + this.options.path = path; + return this; + } + + build() { + const apis: TechdocsAddonTesterApis = [ + [techdocsApiRef, techdocsApi], + [techdocsStorageApiRef, techdocsStorageApi], + [searchApiRef, searchApi], + [scmIntegrationsApiRef, scmIntegrationsApi], + ...this.options.apis, + ]; + + const entityName = { + namespace: + this.options.entity?.metadata?.namespace || + defaultEntity.metadata.namespace, + kind: this.options.entity?.kind || defaultEntity.kind, + name: this.options.entity?.metadata?.name || defaultEntity.metadata.name, + }; + + techdocsApi.getTechDocsMetadata.mockReturnValue( + this.options.metadata || { ...defaultMetadata }, + ); + techdocsApi.getEntityMetadata.mockResolvedValue( + this.options.entity || { ...defaultEntity }, + ); + + techdocsStorageApi.syncEntityDocs.mockResolvedValue('cached'); + techdocsStorageApi.getApiOrigin.mockResolvedValue( + 'https://backstage.example.com/api/techdocs', + ); + + techdocsStorageApi.getEntityDocs.mockResolvedValue( + renderToStaticMarkup(this.options.dom || defaultDom), + ); + + const TechDocsAddonsPage = () => { + return ( + + + } + > + + {this.addons.map((addon, index) => ( + {addon} + ))} + + + + + ); + }; + + return wrapInTestApp(, { + routeEntries: [ + `/docs/${entityName.namespace}/${entityName.kind}/${entityName.name}/${this.options.path}`, + ], + mountedRoutes: { + '/docs': techdocsPlugin.routes.root, + '/docs/:namespace/:kind/:name/*': techdocsPlugin.routes.docRoot, + '/catalog/:namespace/:kind/:name': catalogPlugin.routes.catalogEntity, + }, + }); + } + + // Components using useEffect to perform an asynchronous action (such as fetch) must be rendered within an async + // act call to properly get the final state, even with mocked responses. This utility method makes the signature a bit + // cleaner, since act doesn't return the result of the evaluated function. + // https://github.com/testing-library/react-testing-library/issues/281 + // https://github.com/facebook/react/pull/14853 + async renderWithEffects(): Promise< + typeof screen & { shadowRoot: ShadowRoot | null } + > { + await act(async () => { + render(this.build()); + }); + + const shadowHost = screen.getByTestId('techdocs-native-shadowroot'); + + return { + ...screen, + shadowRoot: shadowHost?.shadowRoot || null, + }; + } +} + +export default TechDocsAddonTester.buildAddonsInTechDocs; diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 531e68ad7a..090f2a24b0 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -53,6 +53,7 @@ "@backstage/cli": "^0.17.1-next.0", "@backstage/core-app-api": "^1.0.1", "@backstage/dev-utils": "^1.0.2-next.0", + "@backstage/plugin-techdocs-addons-test-utils": "^0.0.0", "@backstage/test-utils": "^1.0.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^12.1.3", diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx index 0a1a64218a..bb2de22350 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.tsx @@ -66,7 +66,7 @@ const getUrl = (repository: Repository, template: ReportIssueTemplate) => { if (type === 'github') { return `${url}/issues/new?title=${encodedTitle}&body=${encodedBody}`; } - return `${url}/issues/new?[title]=${encodedTitle}&[body]=${encodedBody}`; + return `${url}/issues/new?issue[title]=${encodedTitle}&issue[description]=${encodedBody}`; }; export const IssueLink = ({ template, repository }: IssueLinkProps) => { diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx new file mode 100644 index 0000000000..68bde5c762 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/ReportIssue.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; + +import React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; + +import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { ReportIssue } from '..'; + +const byUrl = jest.fn(); + +const fireSelectionChangeEvent = (window: Window) => { + const selectionChangeEvent = window.document.createEvent('Event'); + selectionChangeEvent.initEvent('selectionchange', true, true); + window.document.addEventListener('selectionchange', () => {}, false); + fireEvent(window.document, selectionChangeEvent); +}; + +describe('ReportIssue', () => { + const selection = { + type: 'Range', + rangeCount: 1, + isCollapsed: true, + getRangeAt: () => ({ + startContainer: 'this is a sentence', + endContainer: 'this is a sentence', + startOffset: 1, + endOffset: 3, + getBoundingClientRect: () => ({ + right: 100, + top: 100, + width: 100, + height: 100, + }), + }), + toString: () => 'his ', + containsNode: () => true, + } as unknown as Selection; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders github link without exploding', async () => { + byUrl.mockReturnValue({ type: 'github' }); + const { shadowRoot, getByText } = + await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom( + + + +
+
+
+ + + , + ) + .withApis([[scmIntegrationsApiRef, { byUrl }]]) + .renderWithEffects(); + + (shadowRoot as ShadowRoot & Pick).getSelection = + () => selection; + + await waitFor(() => { + expect(getByText('Edit page')).toBeInTheDocument(); + }); + + fireSelectionChangeEvent(window); + + await waitFor(() => { + const link = getByText('Open new Github issue'); + expect(link).toHaveAttribute( + 'href', + 'https://github.com/backstage/backstage/issues/new?title=Documentation%20feedback%3A%20his%20&body=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20his%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgithub.com%2Fbackstage%2Fbackstage%2Fblob%2Fmaster%2Fdocs%2FREADME.md%3E', + ); + }); + }); + + it('renders gitlab link without exploding', async () => { + byUrl.mockReturnValue({ type: 'gitlab' }); + const { shadowRoot, getByText } = + await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom( + + + +
+
+
+ + + , + ) + .withApis([[scmIntegrationsApiRef, { byUrl }]]) + .renderWithEffects(); + + (shadowRoot as ShadowRoot & Pick).getSelection = + () => selection; + + await waitFor(() => { + expect(getByText('Edit page')).toBeInTheDocument(); + }); + + fireSelectionChangeEvent(window); + + await waitFor(() => { + const link = getByText('Open new Gitlab issue'); + expect(link).toHaveAttribute( + 'href', + 'https://gitlab.com/backstage/backstage/issues/new?issue[title]=Documentation%20feedback%3A%20his%20&issue[description]=%23%23%20Documentation%20Feedback%20%F0%9F%93%9D%0A%0A%20%23%23%23%23%20The%20highlighted%20text%3A%20%0A%0A%20%3E%20his%0A%0A%20%23%23%23%23%20The%20comment%20on%20the%20text%3A%20%0A%20_%3Ereplace%20this%20line%20with%20your%20comment%3C_%0A%0A%20___%0ABackstage%20URL%3A%20%3Chttp%3A%2F%2Flocalhost%2F%3E%20%0AMarkdown%20URL%3A%20%3Chttps%3A%2F%2Fgitlab.com%2Fbackstage%2Fbackstage%2F-%2Fblob%2Fmaster%2Fdocs%2FREADME.md%3E', + ); + }); + }); + + it('renders using a custom template builder', async () => { + byUrl.mockReturnValue({ type: 'gitlab' }); + + const templateBuilder = (options: { selection: Selection }) => ({ + title: 'Custom', + body: options.selection.toString().trim(), + }); + + const { shadowRoot, getByText } = + await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom( + + + +
+
+
+ + + , + ) + .withApis([[scmIntegrationsApiRef, { byUrl }]]) + .renderWithEffects(); + + (shadowRoot as ShadowRoot & Pick).getSelection = + () => selection; + + await waitFor(() => { + expect(getByText('Edit page')).toBeInTheDocument(); + }); + + fireSelectionChangeEvent(window); + + await waitFor(() => { + const link = getByText('Open new Gitlab issue'); + expect(link).toHaveAttribute( + 'href', + 'https://gitlab.com/backstage/backstage/issues/new?issue[title]=Custom&issue[description]=his', + ); + }); + }); +}); diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index 264d59146e..fa38feef14 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -22,6 +22,9 @@ export function createTechDocsAddonExtension( // @alpha (undocumented) export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue; +// @public +export type SyncResult = 'cached' | 'updated'; + // @alpha export const TECHDOCS_ADDONS_WRAPPER_KEY = 'techdocs.addons.wrapper.v1'; @@ -103,6 +106,32 @@ export type TechDocsReaderPageValue = { onReady?: () => void; }; +// @public +export interface TechDocsStorageApi { + // (undocumented) + getApiOrigin(): Promise; + // (undocumented) + getBaseUrl( + oldBaseUrl: string, + entityId: CompoundEntityRef, + path: string, + ): Promise; + // (undocumented) + getBuilder(): Promise; + // (undocumented) + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; + // (undocumented) + getStorageUrl(): Promise; + // (undocumented) + syncEntityDocs( + entityId: CompoundEntityRef, + logHandler?: (line: string) => void, + ): Promise; +} + +// @public +export const techdocsStorageApiRef: ApiRef; + // @alpha export const useShadowRoot: () => ShadowRoot | undefined; diff --git a/plugins/techdocs-react/src/api.ts b/plugins/techdocs-react/src/api.ts index 0d3c0b931a..6d726fae01 100644 --- a/plugins/techdocs-react/src/api.ts +++ b/plugins/techdocs-react/src/api.ts @@ -39,3 +39,40 @@ export interface TechDocsApi { export const techdocsApiRef = createApiRef({ id: 'plugin.techdocs.service', }); + +/** + * The outcome of a docs sync operation. + * + * @public + */ +export type SyncResult = 'cached' | 'updated'; + +/** + * API which talks to TechDocs storage to fetch files to render. + * + * @public + */ +export interface TechDocsStorageApi { + getApiOrigin(): Promise; + getStorageUrl(): Promise; + getBuilder(): Promise; + getEntityDocs(entityId: CompoundEntityRef, path: string): Promise; + syncEntityDocs( + entityId: CompoundEntityRef, + logHandler?: (line: string) => void, + ): Promise; + getBaseUrl( + oldBaseUrl: string, + entityId: CompoundEntityRef, + path: string, + ): Promise; +} + +/** + * Utility API reference for the {@link TechDocsStorageApi}. + * + * @public + */ +export const techdocsStorageApiRef = createApiRef({ + id: 'plugin.techdocs.storageservice', +}); diff --git a/plugins/techdocs-react/src/index.ts b/plugins/techdocs-react/src/index.ts index 8b46c9d717..c1cc3a1791 100644 --- a/plugins/techdocs-react/src/index.ts +++ b/plugins/techdocs-react/src/index.ts @@ -26,8 +26,8 @@ export { TechDocsAddons, TECHDOCS_ADDONS_WRAPPER_KEY, } from './addons'; -export { techdocsApiRef } from './api'; -export type { TechDocsApi } from './api'; +export { techdocsApiRef, techdocsStorageApiRef } from './api'; +export type { SyncResult, TechDocsApi, TechDocsStorageApi } from './api'; export { defaultTechDocsReaderPageValue, TechDocsReaderPageProvider, diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index a7c287a4f7..131a3a062a 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -191,7 +191,7 @@ export type ReaderState = { // @public export const Router: () => JSX.Element; -// @public +// @public @deprecated export type SyncResult = 'cached' | 'updated'; // @public @@ -394,7 +394,7 @@ export type TechDocsSearchResultListItemProps = { title?: string; }; -// @public +// @public @deprecated export interface TechDocsStorageApi { // (undocumented) getApiOrigin(): Promise; @@ -417,7 +417,7 @@ export interface TechDocsStorageApi { ): Promise; } -// @public +// @public @deprecated export const techdocsStorageApiRef: ApiRef; // @public diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 13bd654b4d..0c60b51511 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -25,6 +25,7 @@ import { createApiRef } from '@backstage/core-plugin-api'; * Utility API reference for the {@link TechDocsStorageApi}. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', @@ -44,6 +45,7 @@ export const techdocsApiRef = createApiRef({ * The outcome of a docs sync operation. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export type SyncResult = 'cached' | 'updated'; @@ -51,6 +53,7 @@ export type SyncResult = 'cached' | 'updated'; * API which talks to TechDocs storage to fetch files to render. * * @public + * @deprecated Import from `@backstage/plugin-techdocs-react` instead */ export interface TechDocsStorageApi { getApiOrigin(): Promise; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 2e3967cb6d..4ad8b98e37 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { techdocsApiRef, techdocsStorageApiRef } from './api'; +import { + techdocsApiRef, + techdocsStorageApiRef, +} from '@backstage/plugin-techdocs-react'; import { TechDocsClient, TechDocsStorageClient } from './client'; import { rootDocsRouteRef, diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index e8d15e5231..7a373ab302 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -26,7 +26,7 @@ import { useApi, configApiRef } from '@backstage/core-plugin-api'; import { SidebarPinStateContext } from '@backstage/core-components'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { techdocsStorageApiRef } from '../../../api'; +import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react'; import { useTechDocsReader } from './context'; diff --git a/plugins/techdocs/src/reader/components/useReaderState.ts b/plugins/techdocs/src/reader/components/useReaderState.ts index 39f09af264..c9dc9b5dbf 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.ts +++ b/plugins/techdocs/src/reader/components/useReaderState.ts @@ -18,7 +18,7 @@ import { useApi } from '@backstage/core-plugin-api'; import { useMemo, useReducer, useRef } from 'react'; import useAsync from 'react-use/lib/useAsync'; import useAsyncRetry from 'react-use/lib/useAsyncRetry'; -import { techdocsStorageApiRef } from '../../api'; +import { techdocsStorageApiRef } from '@backstage/plugin-techdocs-react'; /** * @public diff --git a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts index 13befb3309..9c7865936c 100644 --- a/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts +++ b/plugins/techdocs/src/reader/transformers/addGitFeedbackLink.ts @@ -48,8 +48,9 @@ export const addGitFeedbackLink = ( } // topmost h1 only contains title for whole page - const title = (dom.querySelector('article>h1') as HTMLElement).childNodes[0] - .textContent; + const title = + (dom.querySelector('article>h1') as HTMLElement)?.childNodes[0] + .textContent || ''; const issueTitle = encodeURIComponent(`Documentation Feedback: ${title}`); const issueDesc = encodeURIComponent( `Page source:\n${sourceAnchor.href}\n\nFeedback:`, diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 6d3ffb8eb2..f4a7a4dd3b 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -259,7 +259,9 @@ const NO_WARNING_PACKAGES = [ 'plugins/search-common', 'plugins/search-react', 'plugins/techdocs', + 'plugins/techdocs-addons-test-utils', 'plugins/techdocs-backend', + 'plugins/techdocs-module-addons-contrib', 'plugins/techdocs-node', 'plugins/techdocs-react', 'plugins/tech-insights', diff --git a/yarn.lock b/yarn.lock index 4ad5693b37..89eaa24e1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23576,6 +23576,11 @@ testcontainers@^8.1.2: ssh-remote-port-forward "^1.0.4" tar-fs "^2.1.1" +testing-library__dom@^7.29.4-beta.1: + version "7.29.4-beta.1" + resolved "https://registry.npmjs.org/testing-library__dom/-/testing-library__dom-7.29.4-beta.1.tgz#dc755f485837e923efbe12c1b7ae43b0ed326f96" + integrity sha512-vb/SMg8rXYcYYFY2eQ2n2a0p2VWNAseM4WHLfsckIyLwxRz5fYqKysUzUYpAX8SwRfruRK+tZqLuL4ND+D1s7Q== + text-extensions@^1.0.0: version "1.9.0" resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26"