diff --git a/.changeset/thick-forks-prove.md b/.changeset/thick-forks-prove.md new file mode 100644 index 0000000000..5025852e79 --- /dev/null +++ b/.changeset/thick-forks-prove.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +--- + +Move `CategoryPicker` from `scaffolder` into `scaffolder-react` +Move `ContextMenu` into `scaffolder-react` and rename it to `ScaffolderPageContextMenu` diff --git a/.changeset/thin-spoons-prove.md b/.changeset/thin-spoons-prove.md new file mode 100644 index 0000000000..a9bf3052f5 --- /dev/null +++ b/.changeset/thin-spoons-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +To offer better customization options, `ScaffolderPageContextMenu` takes callbacks as props instead of booleans diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index 35029cb796..c51f7ba518 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -132,6 +132,18 @@ export type ReviewStateProps = { formState: JsonObject; }; +// @alpha (undocumented) +export function ScaffolderPageContextMenu( + props: ScaffolderPageContextMenuProps, +): JSX.Element | null; + +// @alpha (undocumented) +export type ScaffolderPageContextMenuProps = { + onEditorClicked?: () => void; + onActionsClicked?: () => void; + onTasksClicked?: () => void; +}; + // @alpha export const Stepper: (stepperProps: StepperProps) => JSX.Element; @@ -190,6 +202,9 @@ export interface TemplateCardProps { template: TemplateEntityV1beta3; } +// @alpha +export const TemplateCategoryPicker: () => JSX.Element | null; + // @alpha export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element; diff --git a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx similarity index 75% rename from plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx rename to plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx index c3c2abc39f..fc3c0249a0 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx +++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { useRouteRef } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import IconButton from '@material-ui/core/IconButton'; import ListItemIcon from '@material-ui/core/ListItemIcon'; @@ -28,12 +27,6 @@ import Edit from '@material-ui/icons/Edit'; import List from '@material-ui/icons/List'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - actionsRouteRef, - editRouteRef, - scaffolderListTaskRouteRef, -} from '../../routes'; const useStyles = makeStyles((theme: BackstageTheme) => ({ button: { @@ -41,26 +34,26 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ }, })); +/** + * @alpha + */ export type ScaffolderPageContextMenuProps = { - editor?: boolean; - actions?: boolean; - tasks?: boolean; + onEditorClicked?: () => void; + onActionsClicked?: () => void; + onTasksClicked?: () => void; }; -export function ContextMenu(props: ScaffolderPageContextMenuProps) { +/** + * @alpha + */ +export function ScaffolderPageContextMenu( + props: ScaffolderPageContextMenuProps, +) { + const { onEditorClicked, onActionsClicked, onTasksClicked } = props; const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(); - const editLink = useRouteRef(editRouteRef); - const actionsLink = useRouteRef(actionsRouteRef); - const tasksLink = useRouteRef(scaffolderListTaskRouteRef); - const navigate = useNavigate(); - - const showEditor = props.editor !== false; - const showActions = props.actions !== false; - const showTasks = props.tasks !== false; - - if (!showEditor && !showActions) { + if (!onEditorClicked && !onActionsClicked) { return null; } @@ -93,24 +86,24 @@ export function ContextMenu(props: ScaffolderPageContextMenuProps) { transformOrigin={{ vertical: 'top', horizontal: 'right' }} > - {showEditor && ( - navigate(editLink())}> + {onEditorClicked && ( + )} - {showActions && ( - navigate(actionsLink())}> + {onActionsClicked && ( + )} - {showTasks && ( - navigate(tasksLink())}> + {onTasksClicked && ( + diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/index.ts b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/index.ts new file mode 100644 index 0000000000..ae6774a1e6 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +export { + ScaffolderPageContextMenu, + type ScaffolderPageContextMenuProps, +} from './ScaffolderPageContextMenu'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx similarity index 93% rename from plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx index f02ca51354..ce6e342809 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { useEntityTypeFilter } from '@backstage/plugin-catalog-react'; -import { CategoryPicker } from './CategoryPicker'; +import { TemplateCategoryPicker } from './TemplateCategoryPicker'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { alertApiRef } from '@backstage/core-plugin-api'; import { fireEvent } from '@testing-library/react'; @@ -25,7 +25,7 @@ jest.mock('@backstage/plugin-catalog-react', () => ({ useEntityTypeFilter: jest.fn(), })); -describe('CategoryPicker', () => { +describe('TemplateCategoryPicker', () => { const mockAlertApi = { post: jest.fn() }; beforeEach(() => { @@ -39,7 +39,7 @@ describe('CategoryPicker', () => { await renderInTestApp( - + , ); @@ -56,7 +56,7 @@ describe('CategoryPicker', () => { const { findByTestId } = await renderInTestApp( - + , ); @@ -70,7 +70,7 @@ describe('CategoryPicker', () => { const { queryByText } = await renderInTestApp( - + , ); @@ -86,7 +86,7 @@ describe('CategoryPicker', () => { const { getByRole } = await renderInTestApp( - + , ); @@ -108,7 +108,7 @@ describe('CategoryPicker', () => { const { getByRole } = await renderInTestApp( - + , ); @@ -139,7 +139,7 @@ describe('CategoryPicker', () => { const { getByRole } = await renderInTestApp( - + , ); diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx similarity index 97% rename from plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx rename to plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx index 483751f2e2..5d7021bc8c 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx @@ -37,8 +37,10 @@ const checkedIcon = ; /** * The Category Picker that is rendered on the left side for picking * categories and filtering the template list. + * + * @alpha */ -export const CategoryPicker = () => { +export const TemplateCategoryPicker = () => { const alertApi = useApi(alertApiRef); const { error, loading, availableTypes, selectedTypes, setSelectedTypes } = useEntityTypeFilter(); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts new file mode 100644 index 0000000000..cf41f3f7f9 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 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. + */ +export { TemplateCategoryPicker } from './TemplateCategoryPicker'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index 083d88927e..a38b0689d5 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -22,3 +22,5 @@ export * from './TemplateOutputs'; export * from './Form'; export * from './TaskSteps'; export * from './TaskLogStream'; +export * from './TemplateCategoryPicker'; +export * from './ScaffolderPageContextMenu'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx index d5a76d2a47..ee36d876cf 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.test.tsx @@ -135,4 +135,54 @@ describe('TemplateListPage', () => { expect(getByText('Tags')).toBeInTheDocument(); }); + + describe('scaffolder page context menu', () => { + it('should render if context menu props are not set to false', async () => { + const { queryByTestId } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + expect(queryByTestId('menu-button')).toBeInTheDocument(); + }); + + it('should not render if context menu props are set to false', async () => { + const { queryByTestId } = await renderInTestApp( + + + , + { mountedRoutes: { '/': rootRouteRef } }, + ); + expect(queryByTestId('menu-button')).not.toBeInTheDocument(); + }); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 0af20db6c9..424293665b 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -15,7 +15,9 @@ */ import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; +import { useRouteRef } from '@backstage/core-plugin-api'; import { Content, @@ -32,12 +34,19 @@ import { CatalogFilterLayout, UserListPicker, } from '@backstage/plugin-catalog-react'; -import { CategoryPicker } from './CategoryPicker'; +import { + ScaffolderPageContextMenu, + TemplateCategoryPicker, +} from '@backstage/plugin-scaffolder-react/alpha'; + import { RegisterExistingButton } from './RegisterExistingButton'; -import { useRouteRef } from '@backstage/core-plugin-api'; import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; -import { registerComponentRouteRef } from '../../routes'; -import { ContextMenu } from './ContextMenu'; +import { + actionsRouteRef, + editRouteRef, + registerComponentRouteRef, + scaffolderListTaskRouteRef, +} from '../../routes'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ @@ -74,11 +83,30 @@ export const TemplateListPage = (props: TemplateListPageProps) => { groups: givenGroups = [], templateFilter, } = props; + const navigate = useNavigate(); + const editorLink = useRouteRef(editRouteRef); + const actionsLink = useRouteRef(actionsRouteRef); + const tasksLink = useRouteRef(scaffolderListTaskRouteRef); const groups = givenGroups.length ? createGroupsWithOther(givenGroups) : [defaultGroup]; + const scaffolderPageContextMenuProps = { + onEditorClicked: + props?.contextMenu?.editor !== false + ? () => navigate(editorLink()) + : undefined, + onActionsClicked: + props?.contextMenu?.actions !== false + ? () => navigate(actionsLink()) + : undefined, + onTasksClicked: + props?.contextMenu?.tasks !== false + ? () => navigate(tasksLink()) + : undefined, + }; + return ( @@ -87,7 +115,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => { title="Create a new component" subtitle="Create new software components using standard templates in your organization" > - + @@ -110,7 +138,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => { initialFilter="all" availableFilters={['all', 'starred']} /> - +