Skip to content

feat: add template editor to the ui #5963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add rename and delete
  • Loading branch information
kylecarbs committed Feb 5, 2023
commit 3f152cdc36dec70b8992f5119909364011130a95
119 changes: 102 additions & 17 deletions site/src/components/TemplateVersionEditor/FileDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,26 +44,111 @@ export const CreateFileDialog: FC<{
description={
<Stack spacing={1}>
<Typography>
Specify the path to a file to be created. This path can contain slashes too!
Specify the path to a file to be created. This path can contain
slashes too!
</Typography>
<TextField
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter") {
handleConfirm()
}
}}
helperText={error}
name="file-path"
autoComplete="off"
id="file-path"
placeholder="main.tf"
value={pathValue}
onChange={handleChange}
label="File Path"
/>
<TextField
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter") {
handleConfirm()
}
}}
helperText={error}
name="file-path"
autoComplete="off"
id="file-path"
placeholder="main.tf"
value={pathValue}
onChange={handleChange}
label="File Path"
/>
</Stack>
}
/>
)
}

export const DeleteFileDialog: FC<{
onClose: () => void
onConfirm: () => void
open: boolean
filename: string
}> = ({ onClose, onConfirm, open, filename }) => {
return (
<ConfirmDialog
type="delete"
onClose={onClose}
open={open}
onConfirm={onConfirm}
title="Delete File"
description={`Are you sure you want to delete "${filename}"?`}
/>
)
}

export const RenameFileDialog: FC<{
onClose: () => void
onConfirm: (filename: string) => void
checkExists: (path: string) => boolean
open: boolean
filename: string
}> = ({ checkExists, onClose, onConfirm, open, filename }) => {
const [pathValue, setPathValue] = useState(filename)
const [error, setError] = useState("")
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setPathValue(event.target.value)
}
const handleConfirm = () => {
if (pathValue === "") {
setError("You must enter a path!")
return
}
if (checkExists(pathValue)) {
setError("File already exists")
return
}
onConfirm(pathValue)
setPathValue("")
}

return (
<ConfirmDialog
open={open}
onClose={() => {
onClose()
setPathValue("")
}}
onConfirm={handleConfirm}
hideCancel={false}
type="success"
cancelText="Cancel"
confirmText="Create"
title="Rename File"
description={
<Stack spacing={1}>
<Typography>
Rename {`"${filename}"`} to something else. This path can contain
slashes too!
</Typography>
<TextField
autoFocus
onKeyDown={(event) => {
if (event.key === "Enter") {
handleConfirm()
}
}}
helperText={error}
name="file-path"
autoComplete="off"
id="file-path"
placeholder="main.tf"
defaultValue={filename}
value={pathValue}
onChange={handleChange}
label="File Path"
/>
</Stack>
}
/>
)
Expand Down
73 changes: 70 additions & 3 deletions site/src/components/TemplateVersionEditor/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import ChevronRightIcon from "@material-ui/icons/ChevronRight"
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"
import TreeView from "@material-ui/lab/TreeView"
import TreeItem from "@material-ui/lab/TreeItem"

import { FC, useMemo } from "react"
import Menu from "@material-ui/core/Menu"
import MenuItem from "@material-ui/core/MenuItem"
import { FC, useMemo, useState } from "react"
import { TemplateVersionFiles } from "util/templateVersion"

export interface File {
Expand All @@ -15,9 +16,11 @@ export interface File {

export const FileTree: FC<{
onSelect: (file: File) => void
onDelete: (file: File) => void
onRename: (file: File) => void
files: TemplateVersionFiles
activeFile?: File
}> = ({ activeFile, files, onSelect }) => {
}> = ({ activeFile, files, onDelete, onRename, onSelect }) => {
const styles = useStyles()
const fileTree = useMemo<Record<string, File>>(() => {
const paths = Object.keys(files)
Expand Down Expand Up @@ -55,6 +58,14 @@ export const FileTree: FC<{
})
return roots
}, [files])
const [contextMenu, setContextMenu] = useState<
| {
file: File
clientX: number
clientY: number
}
| undefined
>()

const buildTreeItems = (name: string, file: File): JSX.Element => {
let icon: JSX.Element | null = null
Expand All @@ -81,6 +92,21 @@ export const FileTree: FC<{
onSelect(file)
}
}}
onContextMenu={(event) => {
event.preventDefault()
if (!file.content) {
return
}
setContextMenu(
contextMenu
? undefined
: {
file: file,
clientY: event.clientY,
clientX: event.clientX,
},
)
}}
icon={icon}
>
{Object.entries(file.children || {}).map(([name, file]) => {
Expand All @@ -100,6 +126,47 @@ export const FileTree: FC<{
{Object.entries(fileTree).map(([name, file]) => {
return buildTreeItems(name, file)
})}

<Menu
onClose={() => setContextMenu(undefined)}
open={Boolean(contextMenu)}
anchorReference="anchorPosition"
anchorPosition={contextMenu ? {
top: contextMenu.clientY,
left: contextMenu.clientX,
} : undefined}
anchorOrigin={{
vertical: 'top',
horizontal: 'left',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
>
<MenuItem
onClick={() => {
if (!contextMenu) {
return
}
onRename(contextMenu.file)
setContextMenu(undefined)
}}
>
Rename...
</MenuItem>
<MenuItem
onClick={() => {
if (!contextMenu) {
return
}
onDelete(contextMenu.file)
setContextMenu(undefined)
}}
>
Delete Permanently
</MenuItem>
</Menu>
</TreeView>
)
}
Expand Down
77 changes: 63 additions & 14 deletions site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuild
import { FC, useCallback, useEffect, useState } from "react"
import { navHeight } from "theme/constants"
import { TemplateVersionFiles } from "util/templateVersion"
import { CreateFileDialog } from "./FileDialog"
import {
CreateFileDialog,
DeleteFileDialog,
RenameFileDialog,
} from "./FileDialog"
import { FileTree } from "./FileTree"
import { MonacoEditor } from "./MonacoEditor"

Expand Down Expand Up @@ -59,6 +63,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
const [selectedTab, setSelectedTab] = useState(0)
const [files, setFiles] = useState(initialFiles)
const [createFileOpen, setCreateFileOpen] = useState(false)
const [deleteFileOpen, setDeleteFileOpen] = useState<File>()
const [renameFileOpen, setRenameFileOpen] = useState<File>()
const [activeFile, setActiveFile] = useState<File | undefined>(() => {
const fileKeys = Object.keys(initialFiles)
for (let i = 0; i < fileKeys.length; i++) {
Expand Down Expand Up @@ -188,29 +194,72 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
setCreateFileOpen(false)
}}
/>
<DeleteFileDialog
onConfirm={() => {
if (!deleteFileOpen) {
throw new Error("delete file must be set")
}
const deleted = { ...files }
delete deleted[deleteFileOpen.path]
setFiles(deleted)
setDeleteFileOpen(undefined)
if (activeFile?.path === deleteFileOpen.path) {
setActiveFile(undefined)
}
}}
open={Boolean(deleteFileOpen)}
onClose={() => setDeleteFileOpen(undefined)}
filename={deleteFileOpen?.path || ""}
/>
<RenameFileDialog
open={Boolean(renameFileOpen)}
onClose={() => {
setRenameFileOpen(undefined)
}}
filename={renameFileOpen?.path || ""}
checkExists={(path) => Boolean(files[path])}
onConfirm={(newPath) => {
if (!renameFileOpen) {
return
}
const renamed = { ...files }
renamed[newPath] = renamed[renameFileOpen.path]
delete renamed[renameFileOpen.path]
setFiles(renamed)
renameFileOpen.path = newPath
setActiveFile(renameFileOpen)
setRenameFileOpen(undefined)
}}
/>
</div>
<FileTree
files={files}
onDelete={(file) => setDeleteFileOpen(file)}
onSelect={(file) => setActiveFile(file)}
onRename={(file) => setRenameFileOpen(file)}
activeFile={activeFile}
/>
</div>

<div className={styles.editorPane}>
<div className={styles.editor}>
<MonacoEditor
value={activeFile?.content}
path={activeFile?.path}
onChange={(value) => {
if (!activeFile) {
return
}
setFiles({
...files,
[activeFile.path]: value,
})
}}
/>
{activeFile ? (
<MonacoEditor
value={activeFile?.content}
path={activeFile?.path}
onChange={(value) => {
if (!activeFile) {
return
}
setFiles({
...files,
[activeFile.path]: value,
})
}}
/>
) : (
<div>No file opened</div>
)}
</div>

<div className={styles.panelWrapper}>
Expand Down