From d4cafc837a0ced6d57a49dc3cc5f95be63d2d283 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 1 Oct 2024 13:51:14 +0200 Subject: [PATCH] refactor: standardize template editor layout Signed-off-by: Camila Belo --- .changeset/pretty-buses-repair.md | 5 + plugins/scaffolder/report-alpha.api.md | 22 ++- .../CustomFieldExplorer.tsx | 82 +++++---- .../CustomFieldPlaygroud.tsx | 60 ++++--- .../TemplateEditorPage/CustomFieldsPage.tsx | 19 +- .../DirectoryEditorContext.tsx | 10 +- .../TemplateEditorPage/TemplateEditor.tsx | 134 +++++--------- .../TemplateEditorBrowser.tsx | 16 +- .../TemplateEditorPage/TemplateEditorForm.tsx | 8 +- .../TemplateEditorIntro.tsx | 30 ++-- .../TemplateEditorLayout.tsx | 126 +++++++++++++ .../TemplateEditorPage/TemplateEditorPage.tsx | 80 ++++----- .../TemplateEditorTextArea.tsx | 27 ++- .../TemplateEditorToolbar.tsx | 18 +- .../TemplateEditorToolbarFileMenu.tsx | 109 ++++++++++++ .../TemplateEditorToolbarTemplatesMenu.tsx | 110 ++++++++++++ .../TemplateEditorPage/TemplateFormPage.tsx | 2 + .../TemplateFormPreviewer.tsx | 167 ++++++------------ .../TemplateEditorPage/TemplateIntroPage.tsx | 78 ++++++++ .../TemplateEditorPage/TemplatePage.tsx | 95 ---------- .../components/TemplateEditorPage/index.ts | 4 +- .../useTemplateDirectory.ts | 66 +++++++ .../src/components/Router/Router.tsx | 6 +- .../src/lib/filesystem/WebFileSystemAccess.ts | 3 +- .../lib/filesystem/createExampleTemplate.ts | 1 + .../scaffolder/src/lib/filesystem/index.ts | 6 +- plugins/scaffolder/src/translation.ts | 29 ++- 27 files changed, 836 insertions(+), 477 deletions(-) create mode 100644 .changeset/pretty-buses-repair.md create mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorLayout.tsx create mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx create mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarTemplatesMenu.tsx create mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx delete mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplatePage.tsx create mode 100644 plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts diff --git a/.changeset/pretty-buses-repair.md b/.changeset/pretty-buses-repair.md new file mode 100644 index 0000000000..e5575c36c1 --- /dev/null +++ b/.changeset/pretty-buses-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Standardize template editor pages desktop and mobile layouts. diff --git a/plugins/scaffolder/report-alpha.api.md b/plugins/scaffolder/report-alpha.api.md index 8d581cacba..9acf99f827 100644 --- a/plugins/scaffolder/report-alpha.api.md +++ b/plugins/scaffolder/report-alpha.api.md @@ -173,6 +173,7 @@ export type ScaffolderCustomFieldExplorerClassKey = // @public (undocumented) export type ScaffolderTemplateEditorClassKey = | 'root' + | 'toolbar' | 'browser' | 'editor' | 'preview' @@ -181,6 +182,7 @@ export type ScaffolderTemplateEditorClassKey = // @public (undocumented) export type ScaffolderTemplateFormPreviewerClassKey = | 'root' + | 'toolbar' | 'controls' | 'textArea' | 'preview'; @@ -275,9 +277,13 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs'; readonly 'templateEditorForm.stepper.emptyText': 'There are no spec parameters in the template to preview.'; readonly 'templateTypePicker.title': 'Categories'; - readonly 'templateFormPage.title': 'Template Form Playground'; - readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates and template forms'; - readonly 'templateEditorPage.title': 'Manage Templates'; + readonly 'templateIntroPage.title': 'Manage Templates'; + readonly 'templateIntroPage.subtitle': 'Edit, preview, and try out templates, forms, and custom fields'; + readonly 'templateFormPage.title': 'Template Editor'; + readonly 'templateFormPage.subtitle': 'Edit, preview, and try out templates forms'; + readonly 'templateCustomFieldPage.title': 'Custom Field Explorer'; + readonly 'templateCustomFieldPage.subtitle': 'Edit, preview, and try out custom fields'; + readonly 'templateEditorPage.title': 'Template Editor'; readonly 'templateEditorPage.subtitle': 'Edit, preview, and try out templates and template forms'; readonly 'templateEditorPage.dryRunResults.title': 'Dry-run results'; readonly 'templateEditorPage.dryRunResultsList.title': 'Result {{resultId}}'; @@ -303,12 +309,13 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorPage.templateEditorIntro.createLocal.title': 'Create New Template'; readonly 'templateEditorPage.templateEditorIntro.createLocal.description': 'Create a local template directory, allowing you to both edit and try executing your own template.'; readonly 'templateEditorPage.templateEditorIntro.createLocal.unsupportedTooltip': 'Only supported in some Chromium-based browsers'; - readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template playground'; + readonly 'templateEditorPage.templateEditorIntro.formEditor.title': 'Template Form Playground'; readonly 'templateEditorPage.templateEditorIntro.formEditor.description': 'Preview and edit a template form, either using a sample template or by loading a template from the catalog.'; readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.title': 'Custom Field Explorer'; readonly 'templateEditorPage.templateEditorIntro.fieldExplorer.description': 'View and play around with available installed custom field extensions.'; readonly 'templateEditorPage.templateEditorTextArea.saveIconTooltip': 'Save file'; readonly 'templateEditorPage.templateEditorTextArea.refreshIconTooltip': 'Reload file'; + readonly 'templateEditorPage.templateEditorTextArea.emptyStateParagraph': 'Please select an action on the file menu.'; readonly 'templateEditorPage.templateFormPreviewer.title': 'Load Existing Template'; readonly 'templateListPage.title': 'Create a new component'; readonly 'templateListPage.subtitle': 'Create new software components using standard templates in your organization'; @@ -330,6 +337,11 @@ export const scaffolderTranslationRef: TranslationRef< readonly 'templateEditorToolbar.addToCatalogDialogContent.stepsListItems': 'Save the template files in a local directory\nCreate a pull request to a new or existing git repository\nIf the template already exists, the changes will be reflected in the software catalog once the pull request gets merged\nBut if you are creating a new template, follow the documentation linked below to register the new template repository in software catalog'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationUrl': 'https://backstage.io/docs/features/software-templates/adding-templates/'; readonly 'templateEditorToolbar.addToCatalogDialogActions.documentationButton': 'Go to the documentation'; + readonly 'templateEditorToolbarFileMenu.button': 'File'; + readonly 'templateEditorToolbarFileMenu.options.openDirectory': 'Open template directory'; + readonly 'templateEditorToolbarFileMenu.options.createDirectory': 'Create template directory'; + readonly 'templateEditorToolbarFileMenu.options.closeEditor': 'Close template editor'; + readonly 'templateEditorToolbarTemplatesMenu.button': 'Templates'; } >; @@ -370,7 +382,7 @@ export type TemplateWizardPageProps = { // Warnings were encountered during analysis: // // src/alpha/components/TemplateEditorPage/CustomFieldExplorer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderCustomFieldExplorerClassKey". -// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:6:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey". +// src/alpha/components/TemplateEditorPage/TemplateEditor.d.ts:5:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateEditorClassKey". // src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.d.ts:4:1 - (ae-undocumented) Missing documentation for "ScaffolderTemplateFormPreviewerClassKey". // src/alpha/components/TemplateListPage/TemplateListPage.d.ts:7:1 - (ae-undocumented) Missing documentation for "TemplateListPageProps". // src/alpha/components/TemplateWizardPage/TemplateWizardPage.d.ts:6:1 - (ae-undocumented) Missing documentation for "TemplateWizardPageProps". diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx index daa014078d..0dacb00271 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -19,13 +19,7 @@ import Button from '@material-ui/core/Button'; import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import CardHeader from '@material-ui/core/CardHeader'; -import FormControl from '@material-ui/core/FormControl'; -import IconButton from '@material-ui/core/IconButton'; -import InputLabel from '@material-ui/core/InputLabel'; -import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; import { makeStyles } from '@material-ui/core/styles'; -import CloseIcon from '@material-ui/icons/Close'; import CodeMirror from '@uiw/react-codemirror'; import React, { useCallback, useMemo, useState } from 'react'; import yaml from 'yaml'; @@ -35,6 +29,10 @@ import validator from '@rjsf/validator-ajv8'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import TextField from '@material-ui/core/TextField'; +import SearchIcon from '@material-ui/icons/Search'; +import Autocomplete from '@material-ui/lab/Autocomplete'; /** @public */ export type ScaffolderCustomFieldExplorerClassKey = @@ -48,25 +46,35 @@ const useStyles = makeStyles( root: { gridArea: 'pageContent', display: 'grid', + gridGap: theme.spacing(2), gridTemplateAreas: ` + "controls" + "fieldForm" + "preview" + `, + [theme.breakpoints.up('md')]: { + gridTemplateAreas: ` "controls controls" "fieldForm preview" `, - gridTemplateRows: 'auto 1fr', - gridTemplateColumns: '1fr 1fr', + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, }, controls: { gridArea: 'controls', display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', - margin: theme.spacing(1), }, fieldForm: { gridArea: 'fieldForm', }, preview: { gridArea: 'preview', + display: 'grid', + gridGap: theme.spacing(2), + alignContent: 'start', }, }), { name: 'ScaffolderCustomFieldExplorer' }, @@ -74,10 +82,8 @@ const useStyles = makeStyles( export const CustomFieldExplorer = ({ customFieldExtensions = [], - onClose, }: { customFieldExtensions?: FieldExtensionOptions[]; - onClose?: () => void; }) => { const classes = useStyles(); const { t } = useTranslationRef(scaffolderTranslationRef); @@ -131,29 +137,39 @@ export const CustomFieldExplorer = ({ return (
- - - {t('templateEditorPage.customFieldExplorer.selectFieldLabel')} - - - - - - - + }} + disableClearable + fullWidth + />
diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldPlaygroud.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldPlaygroud.tsx index 3b6fe53d8e..24e39ee8fa 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldPlaygroud.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldPlaygroud.tsx @@ -22,16 +22,16 @@ import { StreamLanguage } from '@codemirror/language'; import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; import { makeStyles } from '@material-ui/core/styles'; -import FormControl from '@material-ui/core/FormControl'; -import InputLabel from '@material-ui/core/InputLabel'; -import MenuItem from '@material-ui/core/MenuItem'; -import Select from '@material-ui/core/Select'; import Accordion from '@material-ui/core/Accordion'; import AccordionSummary from '@material-ui/core/AccordionSummary'; import AccordionDetails from '@material-ui/core/AccordionDetails'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import TextField from '@material-ui/core/TextField'; import Button from '@material-ui/core/Button'; +import InputAdornment from '@material-ui/core/InputAdornment'; import Typography from '@material-ui/core/Typography'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import SearchIcon from '@material-ui/icons/Search'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { Form } from '@backstage/plugin-scaffolder-react/alpha'; @@ -48,7 +48,7 @@ const useStyles = makeStyles( gridTemplateRows: 'auto 1fr', }, controls: { - marginBottom: theme.spacing(2), + marginBottom: theme.spacing(3), }, code: { width: '100%', @@ -114,25 +114,39 @@ export const CustomFieldPlaygroud = ({ return (
- - - {t('templateEditorPage.customFieldExplorer.selectFieldLabel')} - - - + }} + disableClearable + fullWidth + />
diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx index 2d13847339..f49b2ddf05 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/CustomFieldsPage.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ -import React, { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; +import React from 'react'; import { Page, Header, Content } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -32,25 +31,19 @@ interface CustomFieldsPageProps { } export function CustomFieldsPage(props: CustomFieldsPageProps) { - 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/alpha/components/TemplateEditorPage/DirectoryEditorContext.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/DirectoryEditorContext.tsx index ebc5ab10ba..bca2e04fde 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/DirectoryEditorContext.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/DirectoryEditorContext.tsx @@ -209,7 +209,11 @@ export function DirectoryEditorProvider(props: DirectoryEditorProviderProps) { const { directory } = props; const [{ result, error }, { execute }] = useAsync( - async (dir: TemplateDirectoryAccess) => { + async (dir?: TemplateDirectoryAccess) => { + if (!dir) { + return undefined; + } + const manager = new DirectoryEditorManager(dir); await manager.reload(); @@ -223,9 +227,7 @@ export function DirectoryEditorProvider(props: DirectoryEditorProviderProps) { ); useEffect(() => { - if (directory) { - execute(directory); - } + execute(directory); }, [execute, directory]); if (error) { diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx index 48501e6c6f..f2b9e5d7e2 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditor.tsx @@ -13,126 +13,84 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { makeStyles } from '@material-ui/core/styles'; import React, { useState } from 'react'; -import Paper from '@material-ui/core/Paper'; import type { FormProps, LayoutOptions, } from '@backstage/plugin-scaffolder-react'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; -import { TemplateDirectoryAccess } from '../../../lib/filesystem'; import { DirectoryEditorProvider } from './DirectoryEditorContext'; +import { + TemplateEditorLayout, + TemplateEditorLayoutToolbar, + TemplateEditorLayoutBrowser, + TemplateEditorLayoutFiles, + TemplateEditorLayoutPreview, + TemplateEditorLayoutConsole, +} from './TemplateEditorLayout'; import { TemplateEditorToolbar } from './TemplateEditorToolbar'; +import { TemplateEditorToolbarFileMenu } from './TemplateEditorToolbarFileMenu'; import { TemplateEditorBrowser } from './TemplateEditorBrowser'; import { DryRunProvider } from './DryRunContext'; import { TemplateEditorTextArea } from './TemplateEditorTextArea'; import { TemplateEditorForm } from './TemplateEditorForm'; import { DryRunResults } from './DryRunResults'; +import { useTemplateDirectory } from './useTemplateDirectory'; /** @public */ export type ScaffolderTemplateEditorClassKey = | 'root' + | 'toolbar' | 'browser' | 'editor' | 'preview' | 'results'; -const useStyles = makeStyles( - theme => ({ - // Reset and fix sizing to make sure scrolling behaves correctly - root: { - height: '100%', - gridArea: 'pageContent', - display: 'grid', - gridTemplateAreas: ` - "toolbar toolbar toolbar" - "browser editor preview" - "results results results" - `, - gridTemplateColumns: '1fr 3fr 2fr', - gridTemplateRows: 'auto 1fr auto', - }, - toolbar: { - gridArea: 'toolbar', - }, - browser: { - gridArea: 'browser', - overflow: 'auto', - }, - editor: { - gridArea: 'editor', - overflow: 'auto', - borderLeft: `1px solid ${theme.palette.divider}`, - }, - 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), - overflow: 'auto', - }, - results: { - gridArea: 'results', - }, - }), - { name: 'ScaffolderTemplateEditor' }, -); - export const TemplateEditor = (props: { - directory?: TemplateDirectoryAccess; - fieldExtensions?: FieldExtensionOptions[]; layouts?: LayoutOptions[]; - onClose?: () => void; formProps?: FormProps; - onLoad?: () => void; + fieldExtensions?: FieldExtensionOptions[]; }) => { - const classes = useStyles(); + const { layouts, formProps, fieldExtensions } = props; const [errorText, setErrorText] = useState(); + const { + directory, + handleOpenDirectory, + handleCreateDirectory, + handleCloseDirectory, + } = useTemplateDirectory(); return ( - + - -
- -
-
- -
-
- -
-
-
- + + + -
-
-
+ + + + + + + + + + + + -
-
+ +
); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorBrowser.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorBrowser.tsx index b478502f6e..8be58e45df 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorBrowser.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorBrowser.tsx @@ -64,6 +64,10 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) { props.onClose(); }; + if (!directoryEditor) { + return null; + } + return ( <> @@ -72,8 +76,8 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) { > !file.dirty)} - onClick={() => directoryEditor?.save()} + disabled={directoryEditor.files.every(file => !file.dirty)} + onClick={() => directoryEditor.save()} > @@ -83,7 +87,7 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) { 'templateEditorPage.templateEditorBrowser.reloadIconTooltip', )} > - directoryEditor?.reload()}> + directoryEditor.reload()}> @@ -101,9 +105,9 @@ export function TemplateEditorBrowser(props: { onClose?: () => void }) { file.path) ?? []} + selected={directoryEditor.selectedFile?.path ?? ''} + onSelect={directoryEditor.setSelectedFile} + filePaths={directoryEditor.files.map(file => file.path) ?? []} /> ); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorForm.tsx index 56ebaabb83..4ae87a77e9 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorForm.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorForm.tsx @@ -239,7 +239,11 @@ export function TemplateEditorFormDirectoryEditorDryRun( ? selectedFile.content : undefined; - return directoryEditor ? ( + if (!directoryEditor) { + return null; + } + + return ( - ) : null; + ); } TemplateEditorForm.DirectoryEditorDryRun = diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorIntro.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorIntro.tsx index e5edd2cbd5..f0406a58fa 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorIntro.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorIntro.tsx @@ -40,13 +40,13 @@ const useStyles = makeStyles(theme => ({ justifyContent: 'center', }, cardGrid: { - display: 'grid', maxWidth: 1000, - gridTemplateColumns: '1fr 1fr', - gridAutoRows: '1fr 1fr', - gap: '1rem', - [theme.breakpoints.down('sm')]: { - gridAutoFlow: 'row', + display: 'grid', + gridGap: theme.spacing(2), + gridAutoFlow: 'row', + [theme.breakpoints.up('md')]: { + gridTemplateRows: '1fr 1fr', + gridTemplateColumns: '1fr 1fr', }, }, card: { @@ -167,6 +167,15 @@ export function TemplateEditorIntro(props: EditorIntroProps) { Icon={CreateNewFolderIcon} /> + props.onSelect?.('form')} + /> + props.onSelect?.('field-explorer')} /> - - props.onSelect?.('form')} - />
diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorLayout.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorLayout.tsx new file mode 100644 index 0000000000..f77881e79a --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorLayout.tsx @@ -0,0 +1,126 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { WithStyles, withStyles } from '@material-ui/core/styles'; + +export const TemplateEditorLayout = withStyles( + theme => ({ + root: { + height: '100%', + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "toolbar" + "browser" + "editor" + "preview" + "results" + `, + [theme.breakpoints.up('md')]: { + gridTemplateAreas: ` + "toolbar toolbar toolbar" + "browser editor preview" + "results results results" + `, + gridTemplateColumns: '1fr 3fr 2fr', + gridTemplateRows: 'auto 1fr auto', + }, + }, + }), + { name: 'ScaffolderTemplateEditorLayout' }, +)(({ children, classes }: PropsWithChildren) => ( +
{children}
+)); + +export const TemplateEditorLayoutToolbar = withStyles( + { + root: { + gridArea: 'toolbar', + }, + }, + { name: 'ScaffolderTemplateEditorLayoutToolbar' }, +)(({ children, classes }: PropsWithChildren) => ( +
{children}
+)); + +export const TemplateEditorLayoutBrowser = withStyles( + theme => ({ + root: { + gridArea: 'browser', + overflow: 'auto', + [theme.breakpoints.up('md')]: { + borderRight: `1px solid ${theme.palette.divider}`, + }, + }, + }), + { name: 'ScaffolderTemplateEditorLayoutBrowser' }, +)(({ children, classes }: PropsWithChildren) => ( +
{children}
+)); + +export const TemplateEditorLayoutFiles = withStyles( + { + root: { + gridArea: 'editor', + overflow: 'auto', + }, + }, + { name: 'ScaffolderTemplateEditorLayoutFiles' }, +)(({ children, classes }: PropsWithChildren) => ( +
{children}
+)); + +export const TemplateEditorLayoutPreview = withStyles( + theme => ({ + root: { + gridArea: 'preview', + position: 'relative', + backgroundColor: theme.palette.background.default, + [theme.breakpoints.up('md')]: { + borderLeft: `1px solid ${theme.palette.divider}`, + }, + }, + scroll: { + height: '100%', + padding: theme.spacing(1), + [theme.breakpoints.up('md')]: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + overflow: 'auto', + }, + }, + }), + { name: 'ScaffolderTemplateEditorLayoutPreview' }, +)(({ children, classes }: PropsWithChildren) => ( +
+
{children}
+
+)); + +export const TemplateEditorLayoutConsole = withStyles( + { + root: { + gridArea: 'results', + }, + }, + { name: 'ScaffolderTemplateEditorLayoutConsole' }, +)(({ children, classes }: PropsWithChildren) => ( +
{children}
+)); diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx index 90737393f9..b17cc403f1 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Backstage Authors + * 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. @@ -14,63 +14,53 @@ * limitations under the License. */ import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; import { Content, Header, Page } from '@backstage/core-components'; - -import { WebFileSystemAccess } from '../../../lib/filesystem'; - -import { TemplateEditorIntro } from './TemplateEditorIntro'; -import { useNavigate } from 'react-router-dom'; import { useRouteRef } from '@backstage/core-plugin-api'; -import { - editorRouteRef, - customFieldsRouteRef, - rootRouteRef, - templateFormRouteRef, -} from '../../../routes'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { + FormProps, + FieldExtensionOptions, + type LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; import { scaffolderTranslationRef } from '../../../translation'; -import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemAccess'; -import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate'; +import { editRouteRef } from '../../../routes'; +import { TemplateEditor } from './TemplateEditor'; -export function TemplateEditorPage() { - const navigate = useNavigate(); - const createLink = useRouteRef(rootRouteRef); - const editorLink = useRouteRef(editorRouteRef); - const customFieldsLink = useRouteRef(customFieldsRouteRef); - const templateFormLink = useRouteRef(templateFormRouteRef); +const useStyles = makeStyles( + { + content: { + padding: 0, + }, + }, + { name: 'ScaffolderTemplateEditorToolbar' }, +); + +interface TemplatePageProps { + defaultPreviewTemplate?: string; + fieldExtensions?: FieldExtensionOptions[]; + layouts?: LayoutOptions[]; + formProps?: FormProps; +} + +export function TemplateEditorPage(props: TemplatePageProps) { + const classes = useStyles(); + const editLink = useRouteRef(editRouteRef); const { t } = useTranslationRef(scaffolderTranslationRef); return (
- - { - if (option === 'local') { - WebFileSystemAccess.requestDirectoryAccess() - .then(directory => WebFileSystemStore.setDirectory(directory)) - .then(() => navigate(editorLink())) - .catch(() => {}); - } else if (option === 'create-template') { - WebFileSystemAccess.requestDirectoryAccess() - .then(directory => { - createExampleTemplate(directory).then(() => { - WebFileSystemStore.setDirectory(directory); - navigate(editorLink()); - }); - }) - .catch(() => {}); - } else if (option === 'form') { - navigate(templateFormLink()); - } else if (option === 'field-explorer') { - navigate(customFieldsLink()); - } - }} + + diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorTextArea.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorTextArea.tsx index 34788f6cb1..1cca87ca4b 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorTextArea.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorTextArea.tsx @@ -20,7 +20,6 @@ import { showPanel } from '@codemirror/view'; import IconButton from '@material-ui/core/IconButton'; import Paper from '@material-ui/core/Paper'; import Tooltip from '@material-ui/core/Tooltip'; -import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import RefreshIcon from '@material-ui/icons/Refresh'; @@ -45,11 +44,14 @@ const useStyles = makeStyles(theme => ({ verticalAlign: 'top', }, codeMirror: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, + height: '100%', + [theme.breakpoints.up('md')]: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, }, errorPanel: { color: theme.palette.error.main, @@ -150,10 +152,10 @@ export function TemplateEditorTextArea(props: { /** A version of the TemplateEditorTextArea that is connected to the DirectoryEditor context */ export function TemplateEditorDirectoryEditorTextArea(props: { errorText?: string; - onLoad?: () => void; }) { const classes = useStyles(); const directoryEditor = useDirectoryEditor(); + const { t } = useTranslationRef(scaffolderTranslationRef); if (!directoryEditor) { return ( @@ -162,16 +164,7 @@ export function TemplateEditorDirectoryEditorTextArea(props: { color="textSecondary" align="center" > - Please{' '} - - load - {' '} - a template directory. + {t('templateEditorPage.templateEditorTextArea.emptyStateParagraph')} ); } diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.tsx index 8e3124f833..b09b4d861c 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbar.tsx @@ -31,19 +31,25 @@ import DialogActions from '@material-ui/core/DialogActions'; import ExtensionIcon from '@material-ui/icons/Extension'; import DescriptionIcon from '@material-ui/icons/Description'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { FieldExtensionOptions } from '@backstage/plugin-scaffolder-react'; import { ActionPageContent } from '../../../components/ActionsPage/ActionsPage'; -import { CustomFieldPlaygroud } from './CustomFieldPlaygroud'; -import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { scaffolderTranslationRef } from '../../../translation'; +import { CustomFieldPlaygroud } from './CustomFieldPlaygroud'; const useStyles = makeStyles( theme => ({ paper: { - width: '40%', + width: '90%', padding: theme.spacing(2), backgroundColor: theme.palette.background.default, + [theme.breakpoints.up('sm')]: { + width: '70%', + }, + [theme.breakpoints.up('md')]: { + width: '50%', + }, }, appbar: { zIndex: 1, @@ -83,11 +89,7 @@ export function TemplateEditorToolbar(props: {
{children}
- + diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx new file mode 100644 index 0000000000..9b8eee6162 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarFileMenu.tsx @@ -0,0 +1,109 @@ +/* + * 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, { MouseEvent, useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; + +import { useRouteRef } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; + +import { editRouteRef } from '../../../routes'; +import { scaffolderTranslationRef } from '../../../translation'; + +export function TemplateEditorToolbarFileMenu(props: { + onOpenDirectory?: () => void; + onCreateDirectory?: () => void; + onCloseDirectory?: () => void; +}) { + const { onOpenDirectory, onCreateDirectory, onCloseDirectory } = props; + const navigate = useNavigate(); + const editLink = useRouteRef(editRouteRef); + const { t } = useTranslationRef(scaffolderTranslationRef); + const [anchorEl, setAnchorEl] = useState(null); + + const handleOpenMenu = useCallback( + (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }, + [setAnchorEl], + ); + + const handleCloseMenu = useCallback(() => { + setAnchorEl(null); + }, [setAnchorEl]); + + const handleOpenDirectory = useCallback(() => { + handleCloseMenu(); + onOpenDirectory?.(); + }, [handleCloseMenu, onOpenDirectory]); + + const handleCreateDirectory = useCallback(() => { + handleCloseMenu(); + onCreateDirectory?.(); + }, [handleCloseMenu, onCreateDirectory]); + + const handleCloseEditor = useCallback(() => { + handleCloseMenu(); + onCloseDirectory?.(); + navigate(editLink()); + }, [handleCloseMenu, onCloseDirectory, navigate, editLink]); + + return ( + <> + + + + {t('templateEditorToolbarFileMenu.options.openDirectory')} + + + {t('templateEditorToolbarFileMenu.options.createDirectory')} + + + {t('templateEditorToolbarFileMenu.options.closeEditor')} + + + + ); +} diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarTemplatesMenu.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarTemplatesMenu.tsx new file mode 100644 index 0000000000..81ffac2c04 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateEditorToolbarTemplatesMenu.tsx @@ -0,0 +1,110 @@ +/* + * 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, { MouseEvent, useCallback, useState } from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; + +import { Entity } from '@backstage/catalog-model'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { scaffolderTranslationRef } from '../../../translation'; + +const ITEM_HEIGHT = 48; + +const useStyles = makeStyles({ + menu: { + maxHeight: ITEM_HEIGHT * 5, + }, +}); + +export type TemplateOption = { + label: string; + value: Entity; +}; + +export function TemplateEditorToolbarTemplatesMenu(props: { + options: TemplateOption[]; + selectedOption?: TemplateOption; + onSelectOption: (option: TemplateOption) => void; +}) { + const { options, selectedOption, onSelectOption } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(null); + const { t } = useTranslationRef(scaffolderTranslationRef); + + const handleOpenMenu = useCallback( + (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }, + [setAnchorEl], + ); + + const handleCloseMenu = useCallback(() => { + setAnchorEl(null); + }, [setAnchorEl]); + + const handleSelectOption = useCallback( + (option: TemplateOption) => { + handleCloseMenu(); + onSelectOption(option); + }, + [handleCloseMenu, onSelectOption], + ); + + return ( + <> + + + {options.map((option, index) => ( + handleSelectOption(option)} + > + {option.label} + + ))} + + + ); +} diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx index b1f98878ad..7558276721 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.tsx @@ -61,6 +61,8 @@ export function TemplateFormPage(props: TemplateFormPageProps) {
{ const classes = useStyles(); - const { t } = useTranslationRef(scaffolderTranslationRef); const alertApi = useApi(alertApiRef); const catalogApi = useApi(catalogApiRef); const [errorText, setErrorText] = useState(); - const [selectedTemplate, setSelectedTemplate] = - useState(null); + const [selectedTemplate, setSelectedTemplate] = useState(); const [templateOptions, setTemplateOptions] = useState([]); const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); - const { loading } = useAsync( + useAsync( () => catalogApi .getEntities({ @@ -196,76 +173,42 @@ export const TemplateFormPreviewer = ({ const handleSelectChange = useCallback( // TODO(Rugvip): Afaik this should be Entity, but didn't want to make runtime changes while fixing types - (selected: any) => { + (selected: TemplateOption) => { setSelectedTemplate(selected); - setTemplateYaml(yaml.stringify(selected.spec)); + setTemplateYaml(yaml.stringify(selected.value.spec)); }, - [setTemplateYaml], + [setSelectedTemplate, setTemplateYaml], ); return ( - <> - {loading && } - -
- - - - - - - - } - 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/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx new file mode 100644 index 0000000000..b9825bc056 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateIntroPage.tsx @@ -0,0 +1,78 @@ +/* + * 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 from 'react'; +import { Content, Header, Page } from '@backstage/core-components'; + +import { WebFileSystemAccess } from '../../../lib/filesystem'; + +import { TemplateEditorIntro } from './TemplateEditorIntro'; +import { useNavigate } from 'react-router-dom'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { + rootRouteRef, + editorRouteRef, + templateFormRouteRef, + customFieldsRouteRef, +} from '../../../routes'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { scaffolderTranslationRef } from '../../../translation'; +import { WebFileSystemStore } from '../../../lib/filesystem/WebFileSystemAccess'; +import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate'; + +export function TemplateIntroPage() { + const navigate = useNavigate(); + const createLink = useRouteRef(rootRouteRef); + const editorLink = useRouteRef(editorRouteRef); + const templateFormLink = useRouteRef(templateFormRouteRef); + const customFieldsLink = useRouteRef(customFieldsRouteRef); + const { t } = useTranslationRef(scaffolderTranslationRef); + + return ( + +
+ + { + if (option === 'local') { + WebFileSystemAccess.requestDirectoryAccess() + .then(directory => WebFileSystemStore.setDirectory(directory)) + .then(() => navigate(editorLink())) + .catch(() => {}); + } else if (option === 'create-template') { + WebFileSystemAccess.requestDirectoryAccess() + .then(directory => { + createExampleTemplate(directory).then(() => { + WebFileSystemStore.setDirectory(directory); + navigate(editorLink()); + }); + }) + .catch(() => {}); + } else if (option === 'form') { + navigate(templateFormLink()); + } else if (option === 'field-explorer') { + navigate(customFieldsLink()); + } + }} + /> + + + ); +} diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplatePage.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplatePage.tsx deleted file mode 100644 index b70ceba163..0000000000 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplatePage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 useAsyncRetry from 'react-use/esm/useAsyncRetry'; - -import { Page, Header, Content, Progress } from '@backstage/core-components'; -import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; -import { - FormProps, - FieldExtensionOptions, - type LayoutOptions, -} from '@backstage/plugin-scaffolder-react'; - -import { scaffolderTranslationRef } from '../../../translation'; -import { - WebFileSystemAccess, - WebFileSystemStore, -} from '../../../lib/filesystem'; -import { TemplateEditor } from './TemplateEditor'; - -import { makeStyles } from '@material-ui/core/styles'; - -const useStyles = makeStyles( - { - content: { - padding: 0, - }, - }, - { name: 'ScaffolderTemplateEditorToolbar' }, -); - -interface TemplatePageProps { - defaultPreviewTemplate?: string; - fieldExtensions?: FieldExtensionOptions[]; - layouts?: LayoutOptions[]; - formProps?: FormProps; -} - -export function TemplatePage(props: TemplatePageProps) { - const classes = useStyles(); - const { t } = useTranslationRef(scaffolderTranslationRef); - - const { value, loading, retry } = useAsyncRetry(async () => { - const directory = await WebFileSystemStore.getDirectory(); - if (!directory) return undefined; - return WebFileSystemAccess.fromHandle(directory); - }, []); - - const handleLoadDirectory = useCallback(() => { - WebFileSystemAccess.requestDirectoryAccess() - .then(WebFileSystemStore.setDirectory) - .then(retry); - }, [retry]); - - const handleCloseDirectory = useCallback(() => { - WebFileSystemStore.setDirectory(undefined).then(retry); - }, [retry]); - - return ( - -
- - {loading ? ( - - ) : ( - - )} - - - ); -} diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/index.ts b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/index.ts index 84ab74da98..f16f2426d7 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/index.ts +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/index.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -export { TemplatePage } from './TemplatePage'; -export { TemplateFormPage } from './TemplateFormPage'; export { TemplateEditorPage } from './TemplateEditorPage'; +export { TemplateFormPage } from './TemplateFormPage'; +export { TemplateIntroPage } from './TemplateIntroPage'; export { CustomFieldsPage } from './CustomFieldsPage'; export type { ScaffolderCustomFieldExplorerClassKey } from './CustomFieldExplorer'; export type { ScaffolderTemplateEditorClassKey } from './TemplateEditor'; diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts new file mode 100644 index 0000000000..812a21e437 --- /dev/null +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/useTemplateDirectory.ts @@ -0,0 +1,66 @@ +/* + * 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 { useCallback } from 'react'; +import useAsyncRetry from 'react-use/esm/useAsyncRetry'; + +import { + WebFileSystemAccess, + WebFileSystemStore, + WebDirectoryAccess, +} from '../../../lib/filesystem'; +import { createExampleTemplate } from '../../../lib/filesystem/createExampleTemplate'; + +export function useTemplateDirectory(): { + directory?: WebDirectoryAccess; + loading: boolean; + error?: Error; + handleOpenDirectory: () => void; + handleCreateDirectory: () => void; + handleCloseDirectory: () => void; +} { + const { value, loading, error, retry } = useAsyncRetry(async () => { + const directory = await WebFileSystemStore.getDirectory(); + if (!directory) return undefined; + return WebFileSystemAccess.fromHandle(directory); + }, []); + + const handleOpenDirectory = useCallback(() => { + WebFileSystemAccess.requestDirectoryAccess() + .then(WebFileSystemStore.setDirectory) + .then(retry); + }, [retry]); + + const handleCreateDirectory = useCallback(() => { + WebFileSystemAccess.requestDirectoryAccess() + .then(createExampleTemplate) + .then(WebFileSystemStore.setDirectory) + .then(retry); + }, [retry]); + + const handleCloseDirectory = useCallback(() => { + WebFileSystemStore.setDirectory(undefined).then(retry); + }, [retry]); + + return { + directory: value, + loading, + error, + handleOpenDirectory, + handleCreateDirectory, + handleCloseDirectory, + }; +} diff --git a/plugins/scaffolder/src/components/Router/Router.tsx b/plugins/scaffolder/src/components/Router/Router.tsx index e79d930795..5766e3d043 100644 --- a/plugins/scaffolder/src/components/Router/Router.tsx +++ b/plugins/scaffolder/src/components/Router/Router.tsx @@ -54,8 +54,8 @@ import { import { TemplateListPage, TemplateWizardPage } from '../../alpha/components'; import { OngoingTask } from '../OngoingTask'; import { - TemplatePage, TemplateFormPage, + TemplateIntroPage, TemplateEditorPage, CustomFieldsPage, } from '../../alpha/components/TemplateEditorPage'; @@ -171,7 +171,7 @@ export const Router = (props: PropsWithChildren) => { path={editRouteRef.path} element={ - + } /> @@ -205,7 +205,7 @@ export const Router = (props: PropsWithChildren) => { path={editorRouteRef.path} element={ - { diff --git a/plugins/scaffolder/src/lib/filesystem/createExampleTemplate.ts b/plugins/scaffolder/src/lib/filesystem/createExampleTemplate.ts index b93d678d5e..e3d0053d69 100644 --- a/plugins/scaffolder/src/lib/filesystem/createExampleTemplate.ts +++ b/plugins/scaffolder/src/lib/filesystem/createExampleTemplate.ts @@ -91,4 +91,5 @@ export async function createExampleTemplate( for (const [name, data] of Object.entries(files)) { await directory.createFile({ name, data }); } + return directory; } diff --git a/plugins/scaffolder/src/lib/filesystem/index.ts b/plugins/scaffolder/src/lib/filesystem/index.ts index 29f72294ea..4f5c9ff08c 100644 --- a/plugins/scaffolder/src/lib/filesystem/index.ts +++ b/plugins/scaffolder/src/lib/filesystem/index.ts @@ -16,4 +16,8 @@ export type { TemplateFileAccess, TemplateDirectoryAccess } from './types'; export { blobToBase64 } from './helpers'; -export { WebFileSystemAccess, WebFileSystemStore } from './WebFileSystemAccess'; +export { + WebDirectoryAccess, + WebFileSystemAccess, + WebFileSystemStore, +} from './WebFileSystemAccess'; diff --git a/plugins/scaffolder/src/translation.ts b/plugins/scaffolder/src/translation.ts index ba59b90c38..7299d3245a 100644 --- a/plugins/scaffolder/src/translation.ts +++ b/plugins/scaffolder/src/translation.ts @@ -195,12 +195,21 @@ export const scaffolderTranslationRef = createTranslationRef({ templateTypePicker: { title: 'Categories', }, + templateIntroPage: { + title: 'Manage Templates', + subtitle: + 'Edit, preview, and try out templates, forms, and custom fields', + }, templateFormPage: { - title: 'Template Form Playground', - subtitle: 'Edit, preview, and try out templates and template forms', + title: 'Template Editor', + subtitle: 'Edit, preview, and try out templates forms', + }, + templateCustomFieldPage: { + title: 'Custom Field Explorer', + subtitle: 'Edit, preview, and try out custom fields', }, templateEditorPage: { - title: 'Manage Templates', + title: 'Template Editor', subtitle: 'Edit, preview, and try out templates and template forms', dryRunResults: { title: 'Dry-run results', @@ -254,7 +263,7 @@ export const scaffolderTranslationRef = createTranslationRef({ unsupportedTooltip: 'Only supported in some Chromium-based browsers', }, formEditor: { - title: 'Template playground', + title: 'Template Form Playground', description: 'Preview and edit a template form, either using a sample template or by loading a template from the catalog.', }, @@ -267,6 +276,7 @@ export const scaffolderTranslationRef = createTranslationRef({ templateEditorTextArea: { saveIconTooltip: 'Save file', refreshIconTooltip: 'Reload file', + emptyStateParagraph: 'Please select an action on the file menu.', }, templateFormPreviewer: { title: 'Load Existing Template', @@ -316,5 +326,16 @@ export const scaffolderTranslationRef = createTranslationRef({ 'https://backstage.io/docs/features/software-templates/adding-templates/', }, }, + templateEditorToolbarFileMenu: { + button: 'File', + options: { + openDirectory: 'Open template directory', + createDirectory: 'Create template directory', + closeEditor: 'Close template editor', + }, + }, + templateEditorToolbarTemplatesMenu: { + button: 'Templates', + }, }, });