scaffolder: extract TemplateEditorTextArea

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-04-28 14:55:57 +02:00
parent 48307fbf0d
commit a788dacc73
2 changed files with 161 additions and 100 deletions
@@ -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: {
<TemplateEditorBrowser onClose={props.onClose} />
</section>
<section className={classes.editor}>
<TemplateEditorTextArea errorText={errorText} />
<TemplateEditorTextArea.DirectoryEditor errorText={errorText} />
</section>
<section className={classes.preview}>
<TemplateEditorForm
@@ -217,68 +189,6 @@ function TemplateEditorBrowser(props: { onClose?: () => 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 (
<>
<CodeMirror
className={classes.editorCodeMirror}
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport), panelExtension]}
value={directoryEditor.selectedFile?.content}
onChange={content =>
directoryEditor.selectedFile?.updateContent(content)
}
/>
{directoryEditor.selectedFile?.dirty && (
<div className={classes.editorFloatingButtons}>
<Paper>
<Tooltip title="Save file">
<IconButton
className={classes.browserButton}
onClick={() => directoryEditor.save()}
>
<SaveIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reload file">
<IconButton
className={classes.browserButton}
onClick={() => directoryEditor.reload()}
>
<RefreshIcon />
</IconButton>
</Tooltip>
</Paper>
</div>
)}
</>
);
}
interface ErrorBoundaryProps {
generation: number;
setErrorText(errorText: string | undefined): void;
@@ -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 (
<div className={classes.container}>
<CodeMirror
className={classes.codeMirror}
theme="dark"
height="100%"
extensions={[StreamLanguage.define(yamlSupport), panelExtension]}
value={props.content}
onChange={props.onUpdate}
/>
{(props.onSave || props.onReload) && (
<div className={classes.floatingButtons}>
<Paper>
{props.onSave && (
<Tooltip title="Save file">
<IconButton
className={classes.floatingButton}
onClick={() => props.onSave?.()}
>
<SaveIcon />
</IconButton>
</Tooltip>
)}
{props.onReload && (
<Tooltip title="Reload file">
<IconButton
className={classes.floatingButton}
onClick={() => props.onReload?.()}
>
<RefreshIcon />
</IconButton>
</Tooltip>
)}
</Paper>
</div>
)}
</div>
);
}
/** 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 (
<TemplateEditorTextArea
errorText={props.errorText}
content={directoryEditor.selectedFile?.content}
onUpdate={content => directoryEditor.selectedFile?.updateContent(content)}
{...actions}
/>
);
}
TemplateEditorTextArea.DirectoryEditor = TemplateEditorDirectoryEditorTextArea;