diff --git a/.changeset/cyan-peaches-lay.md b/.changeset/cyan-peaches-lay.md new file mode 100644 index 0000000000..a2b393caf8 --- /dev/null +++ b/.changeset/cyan-peaches-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Create a separate route for the template form editor so we refresh it without being redirected to scaffolder edit page. diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 81cf0ecced..05255300dd 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -614,6 +614,7 @@ export const scaffolderPlugin: BackstagePlugin< edit: SubRouteRef; editor: SubRouteRef; customFields: SubRouteRef; + templateForm: SubRouteRef; }, { registerComponent: ExternalRouteRef; diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index 27c2eede70..385a5f9fad 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -40,6 +40,7 @@ import { scaffolderListTaskRouteRef, scaffolderTaskRouteRef, selectedTemplateRouteRef, + templateFormRouteRef, } from '../../routes'; import { ErrorPage } from '@backstage/core-components'; @@ -54,6 +55,7 @@ import { TemplateListPage, TemplateWizardPage } from '../../next'; import { OngoingTask } from '../OngoingTask'; import { TemplatePage, + TemplateFormPage, TemplateEditorPage, CustomFieldsPage, } from '../../next/TemplateEditorPage'; @@ -169,11 +171,7 @@ export const Router = (props: PropsWithChildren) => { path={editRouteRef.path} element={ - + } /> @@ -185,6 +183,18 @@ export const Router = (props: PropsWithChildren) => { } /> + + + + } + /> } /> []; - layouts?: LayoutOptions[]; - formProps?: FormProps; -} - -export function TemplateEditorPage(props: TemplateEditorPageProps) { - const [selection, setSelection] = useState(); +export function TemplateEditorPage() { const navigate = useNavigate(); const actionsLink = useRouteRef(actionsRouteRef); const tasksLink = useRouteRef(scaffolderListTaskRouteRef); const createLink = useRouteRef(rootRouteRef); const editorLink = useRouteRef(editorRouteRef); const customFieldsLink = useRouteRef(customFieldsRouteRef); + const templateFormLink = useRouteRef(templateFormRouteRef); const { t } = useTranslationRef(scaffolderTranslationRef); const scaffolderPageContextMenuProps = { @@ -80,19 +50,14 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { onCreateClicked: () => navigate(createLink()), }; - let content: JSX.Element | null = null; - if (selection?.type === 'form') { - content = ( - setSelection(undefined)} - layouts={props.layouts} - formProps={props.formProps} - /> - ); - } else { - content = ( + return ( + +
+ +
{ @@ -111,25 +76,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { }) .catch(() => {}); } else if (option === 'form') { - setSelection({ type: 'form' }); + navigate(templateFormLink()); } else if (option === 'field-explorer') { navigate(customFieldsLink()); } }} /> - ); - } - - return ( - -
- -
- {content}
); } diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorToolbar.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorToolbar.tsx index ef4ac95c3c..1d89616964 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorToolbar.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorToolbar.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; import { makeStyles } from '@material-ui/core/styles'; import AppBar from '@material-ui/core/AppBar'; @@ -49,18 +49,29 @@ const useStyles = makeStyles( }, toolbar: { display: 'grid', - justifyItems: 'flex-end', + gridTemplateColumns: 'auto 1fr', + gridGap: theme.spacing(1), padding: theme.spacing(0, 1), backgroundColor: theme.palette.background.paper, }, + toolbarCustomActions: { + display: 'grid', + alignItems: 'center', + gridAutoFlow: 'Column', + gridGap: theme.spacing(1), + }, + toolbarDefaultActions: { + justifySelf: 'end', + }, }), { name: 'ScaffolderTemplateEditorToolbar' }, ); export function TemplateEditorToolbar(props: { + children?: ReactNode; fieldExtensions?: FieldExtensionOptions[]; }) { - const { fieldExtensions } = props; + const { children, fieldExtensions } = props; const classes = useStyles(); const [showFieldsDrawer, setShowFieldsDrawer] = useState(false); const [showActionsDrawer, setShowActionsDrawer] = useState(false); @@ -69,14 +80,19 @@ export function TemplateEditorToolbar(props: { return ( - +
{children}
+ - - diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPage.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPage.tsx new file mode 100644 index 0000000000..1781be69fb --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPage.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2024 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, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { makeStyles } from '@material-ui/core/styles'; + +import { Page, Header, Content } from '@backstage/core-components'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + FormProps, + LayoutOptions, + FieldExtensionOptions, +} from '@backstage/plugin-scaffolder-react'; + +import { editRouteRef } from '../../routes'; +import { scaffolderTranslationRef } from '../../translation'; + +import { TemplateFormPreviewer } from './TemplateFormPreviewer'; + +const useStyles = makeStyles({ + root: { + padding: 0, + }, +}); + +interface TemplateFormPageProps { + layouts?: LayoutOptions[]; + formProps?: FormProps; + fieldExtensions?: FieldExtensionOptions[]; + defaultPreviewTemplate?: string; +} + +export function TemplateFormPage(props: TemplateFormPageProps) { + const classes = useStyles(); + const navigate = useNavigate(); + const editLink = useRouteRef(editRouteRef); + const { t } = useTranslationRef(scaffolderTranslationRef); + + const handleClose = useCallback(() => { + navigate(editLink()); + }, [navigate, editLink]); + + return ( + +
+ + + + + ); +} diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx index 4f79b7f5c2..e0c40a98be 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -13,18 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, humanizeEntityRef, } from '@backstage/plugin-catalog-react'; -import FormControl from '@material-ui/core/FormControl'; -import IconButton from '@material-ui/core/IconButton'; -import InputLabel from '@material-ui/core/InputLabel'; import LinearProgress from '@material-ui/core/LinearProgress'; -import MenuItem from '@material-ui/core/MenuItem'; +import Paper from '@material-ui/core/Paper'; +import FormControl from '@material-ui/core/FormControl'; +import Input from '@material-ui/core/Input'; import Select from '@material-ui/core/Select'; +import Tooltip from '@material-ui/core/Tooltip'; +import IconButton from '@material-ui/core/IconButton'; +import MenuItem from '@material-ui/core/MenuItem'; import { makeStyles } from '@material-ui/core/styles'; import CloseIcon from '@material-ui/icons/Close'; import React, { useCallback, useState } from 'react'; @@ -35,6 +38,7 @@ import { FieldExtensionOptions, FormProps, } from '@backstage/plugin-scaffolder-react'; +import { TemplateEditorToolbar } from './TemplateEditorToolbar'; import { TemplateEditorForm } from './TemplateEditorForm'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; @@ -94,27 +98,40 @@ export type ScaffolderTemplateFormPreviewerClassKey = const useStyles = makeStyles( theme => ({ root: { + height: '100%', gridArea: 'pageContent', display: 'grid', gridTemplateAreas: ` - "controls controls" + "toolbar toolbar" "textArea preview" `, gridTemplateRows: 'auto 1fr', gridTemplateColumns: '1fr 1fr', }, - controls: { - gridArea: 'controls', - display: 'flex', - flexFlow: 'row nowrap', - alignItems: 'center', - margin: theme.spacing(1), + toolbar: { + gridArea: 'toolbar', }, textArea: { gridArea: 'textArea', + height: '100%', }, preview: { gridArea: 'preview', + position: 'relative', + borderLeft: `1px solid ${theme.palette.divider}`, + backgroundColor: theme.palette.background.default, + }, + scroll: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + padding: theme.spacing(1), + }, + formControl: { + minWidth: 120, + maxWidth: 300, }, }), { name: 'ScaffolderTemplateFormPreviewer' }, @@ -137,8 +154,9 @@ export const TemplateFormPreviewer = ({ const { t } = useTranslationRef(scaffolderTranslationRef); const alertApi = useApi(alertApiRef); const catalogApi = useApi(catalogApiRef); - const [selectedTemplate, setSelectedTemplate] = useState(''); const [errorText, setErrorText] = useState(); + const [selectedTemplate, setSelectedTemplate] = + useState(null); const [templateOptions, setTemplateOptions] = useState([]); const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); @@ -188,29 +206,45 @@ export const TemplateFormPreviewer = ({ return ( <> {loading && } -
-
- - - {t('templateEditorPage.templateFormPreviewer.title')} - - - - - - - + +
+ + + + + + + + } + renderValue={selected => { + if (!selected) { + return t('templateEditorPage.templateFormPreviewer.title'); + } + return (selected as Entity).metadata.title; + }} + inputProps={{ + 'aria-label': t( + 'templateEditorPage.templateFormPreviewer.title', + ), + }} + > + {templateOptions.map((option, index) => ( + + {option.label} + + ))} + + +
- +
+ +
-
+ ); }; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/index.ts b/plugins/scaffolder/src/next/TemplateEditorPage/index.ts index 45e85f8a83..84ab74da98 100644 --- a/plugins/scaffolder/src/next/TemplateEditorPage/index.ts +++ b/plugins/scaffolder/src/next/TemplateEditorPage/index.ts @@ -15,6 +15,7 @@ */ export { TemplatePage } from './TemplatePage'; +export { TemplateFormPage } from './TemplateFormPage'; export { TemplateEditorPage } from './TemplateEditorPage'; export { CustomFieldsPage } from './CustomFieldsPage'; export type { ScaffolderCustomFieldExplorerClassKey } from './CustomFieldExplorer'; diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx index bf4d3a41ac..8b24e86069 100644 --- a/plugins/scaffolder/src/plugin.tsx +++ b/plugins/scaffolder/src/plugin.tsx @@ -70,6 +70,7 @@ import { editRouteRef, editorRouteRef, customFieldsRouteRef, + templateFormRouteRef, } from './routes'; import { MyGroupsPicker, @@ -111,6 +112,7 @@ export const scaffolderPlugin = createPlugin({ edit: editRouteRef, editor: editorRouteRef, customFields: customFieldsRouteRef, + templateForm: templateFormRouteRef, }, externalRoutes: { registerComponent: registerComponentRouteRef, diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 3bd4435078..55344a8b0a 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -86,7 +86,13 @@ export const editorRouteRef = createSubRouteRef({ }); export const customFieldsRouteRef = createSubRouteRef({ - id: 'scaffolder/edit', + id: 'scaffolder/customFields', parent: rootRouteRef, path: '/custom-fields', }); + +export const templateFormRouteRef = createSubRouteRef({ + id: 'scaffolder/editorForm', + parent: rootRouteRef, + path: '/template-form', +});