diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditor.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditor.tsx index e1900357ad..4e1b26441b 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditor.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditor.tsx @@ -13,6 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useApiHolder } from '@backstage/core-plugin-api'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { Divider, IconButton, makeStyles, Tooltip } from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import SaveIcon from '@material-ui/icons/Save'; import React, { Component, ReactNode, @@ -20,37 +26,21 @@ import React, { useReducer, useState, } from 'react'; -import { useKeyboardEvent } from '@react-hookz/web'; import useDebounce from 'react-use/lib/useDebounce'; -import { useApiHolder } from '@backstage/core-plugin-api'; -import { JsonObject, JsonValue } from '@backstage/types'; -import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; -import { showPanel } from '@codemirror/view'; -import { StreamLanguage } from '@codemirror/language'; -import { - IconButton, - makeStyles, - Divider, - Tooltip, - Paper, -} from '@material-ui/core'; -import SaveIcon from '@material-ui/icons/Save'; -import RefreshIcon from '@material-ui/icons/Refresh'; -import CloseIcon from '@material-ui/icons/Close'; -import CodeMirror from '@uiw/react-codemirror'; import yaml from 'yaml'; import { FieldExtensionOptions } from '../../../extensions'; +import { TemplateDirectoryAccess } from '../../../lib/filesystem'; import { TemplateParameterSchema } from '../../../types'; +import { FileBrowser } from '../../FileBrowser'; import { MultistepJsonForm } from '../../MultistepJsonForm'; import { createValidator } from '../../TemplatePage'; -import { TemplateDirectoryAccess } from '../../../lib/filesystem'; -import { FileBrowser } from '../../FileBrowser'; import { DirectoryEditorProvider, useDirectoryEditor, } from './DirectoryEditorContext'; import { DryRunProvider, useDryRun } from './DryRunContext'; import { DryRunResults } from './DryRunResults'; +import { TemplateEditorTextArea } from './TemplateEditorTextArea'; const useStyles = makeStyles(theme => ({ // Reset and fix sizing to make sure scrolling behaves correctly @@ -94,27 +84,9 @@ const useStyles = makeStyles(theme => ({ marginBottom: theme.spacing(1), }, editor: { - position: 'relative', gridArea: 'editor', overflow: 'auto', }, - editorCodeMirror: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - editorErrorPanel: { - color: theme.palette.error.main, - lineHeight: 2, - margin: theme.spacing(0, 1), - }, - editorFloatingButtons: { - position: 'absolute', - top: theme.spacing(1), - right: theme.spacing(3), - }, preview: { gridArea: 'preview', overflow: 'auto', @@ -142,7 +114,7 @@ export const TemplateEditor = (props: {
- +
void }) { ); } -function TemplateEditorTextArea(props: { errorText?: string }) { - const { errorText } = props; - const classes = useStyles(); - const directoryEditor = useDirectoryEditor(); - - const panelExtension = useMemo(() => { - if (!errorText) { - return showPanel.of(null); - } - - const dom = document.createElement('div'); - dom.classList.add(classes.editorErrorPanel); - dom.textContent = errorText; - return showPanel.of(() => ({ dom, bottom: true })); - }, [classes, errorText]); - - useKeyboardEvent( - e => e.key === 's' && (e.ctrlKey || e.metaKey), - e => { - e.preventDefault(); - directoryEditor.save(); - }, - ); - - return ( - <> - - directoryEditor.selectedFile?.updateContent(content) - } - /> - {directoryEditor.selectedFile?.dirty && ( -
- - - directoryEditor.save()} - > - - - - - directoryEditor.reload()} - > - - - - -
- )} - - ); -} - interface ErrorBoundaryProps { generation: number; setErrorText(errorText: string | undefined): void; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditorTextArea.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditorTextArea.tsx new file mode 100644 index 0000000000..3700348ef4 --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditor/TemplateEditorTextArea.tsx @@ -0,0 +1,151 @@ +/* + * 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 { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { showPanel } from '@codemirror/view'; +import { IconButton, makeStyles, Paper, Tooltip } from '@material-ui/core'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import SaveIcon from '@material-ui/icons/Save'; +import { useKeyboardEvent } from '@react-hookz/web'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useMemo } from 'react'; +import { useDirectoryEditor } from './DirectoryEditorContext'; + +const useStyles = makeStyles(theme => ({ + container: { + position: 'relative', + width: '100%', + height: '100%', + }, + codeMirror: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + errorPanel: { + color: theme.palette.error.main, + lineHeight: 2, + margin: theme.spacing(0, 1), + }, + floatingButtons: { + position: 'absolute', + top: theme.spacing(1), + right: theme.spacing(3), + }, + floatingButton: { + padding: theme.spacing(1), + }, +})); + +/** A wrapper around CodeMirror with an error panel and extra actions available */ +export function TemplateEditorTextArea(props: { + content?: string; + onUpdate?: (content: string) => void; + errorText?: string; + onSave?: () => void; + onReload?: () => void; +}) { + const { errorText } = props; + const classes = useStyles(); + + const panelExtension = useMemo(() => { + if (!errorText) { + return showPanel.of(null); + } + + const dom = document.createElement('div'); + dom.classList.add(classes.errorPanel); + dom.textContent = errorText; + return showPanel.of(() => ({ dom, bottom: true })); + }, [classes, errorText]); + + useKeyboardEvent( + e => e.key === 's' && (e.ctrlKey || e.metaKey), + e => { + e.preventDefault(); + if (props.onSave) { + props.onSave(); + } + }, + ); + + return ( +
+ + {(props.onSave || props.onReload) && ( +
+ + {props.onSave && ( + + props.onSave?.()} + > + + + + )} + {props.onReload && ( + + props.onReload?.()} + > + + + + )} + +
+ )} +
+ ); +} + +/** A version of the TemplateEditorTextArea that is connected to the DirectoryEditor context */ +export function TemplateEditorDirectoryEditorTextArea(props: { + errorText?: string; +}) { + const directoryEditor = useDirectoryEditor(); + + const actions = directoryEditor.selectedFile?.dirty + ? { + onSave: () => directoryEditor.save(), + onReload: () => directoryEditor.reload(), + } + : undefined; + + return ( + directoryEditor.selectedFile?.updateContent(content)} + {...actions} + /> + ); +} + +TemplateEditorTextArea.DirectoryEditor = TemplateEditorDirectoryEditorTextArea;