fix(site): keep TemplateVersionEditor file tree in sync (#25068)

This commit is contained in:
McKayla はな
2026-06-26 14:47:55 -06:00
committed by GitHub
parent e24bc5b572
commit 7481e1a5a0
4 changed files with 94 additions and 20 deletions
@@ -31,7 +31,8 @@ const meta: Meta<typeof TemplateVersionEditor> = {
activePath: "main.tf",
template: MockTemplate,
templateVersion: MockTemplateVersion,
defaultFileTree: MockTemplateVersionFileTree,
fileTree: MockTemplateVersionFileTree,
onFileTreeChange: action("onFileTreeChange"),
onPublish: action("onPublish"),
onConfirmPublish: action("onConfirmPublish"),
onCancelPublish: action("onCancelPublish"),
@@ -78,7 +78,8 @@ type Tab = "logs" | "resources" | undefined; // Undefined is to hide the tab
interface TemplateVersionEditorProps {
template: Template;
templateVersion: TemplateVersion;
defaultFileTree: FileTree;
fileTree: FileTree;
onFileTreeChange: (updater: (fileTree: FileTree) => FileTree) => void;
buildLogs?: ProvisionerJobLog[];
resources?: WorkspaceResource[];
isBuilding: boolean;
@@ -108,7 +109,8 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
canPublish,
template,
templateVersion,
defaultFileTree,
fileTree,
onFileTreeChange,
onPreview,
onPublish,
onConfirmPublish,
@@ -133,7 +135,6 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
const navigate = useNavigate();
const getLink = useLinks();
const [selectedTab, setSelectedTab] = useState<Tab>(defaultTab);
const [fileTree, setFileTree] = useState(defaultFileTree);
const [createFileOpen, setCreateFileOpen] = useState(false);
const [deleteFileOpen, setDeleteFileOpen] = useState<string>();
const [renameFileOpen, setRenameFileOpen] = useState<string>();
@@ -351,7 +352,9 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
}}
checkExists={(path) => existsFile(path, fileTree)}
onConfirm={(path) => {
setFileTree((fileTree) => createFile(path, fileTree, ""));
onFileTreeChange((fileTree) =>
createFile(path, fileTree, ""),
);
onActivePathChange(path);
setCreateFileOpen(false);
setDirty(true);
@@ -362,7 +365,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
if (!deleteFileOpen) {
throw new Error("delete file must be set");
}
setFileTree((fileTree) =>
onFileTreeChange((fileTree) =>
removeFile(deleteFileOpen, fileTree),
);
setDeleteFileOpen(undefined);
@@ -387,7 +390,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
if (!renameFileOpen) {
return;
}
setFileTree((fileTree) =>
onFileTreeChange((fileTree) =>
moveFile(renameFileOpen, newPath, fileTree),
);
onActivePathChange(newPath);
@@ -433,7 +436,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
if (!activePath) {
return;
}
setFileTree((fileTree) =>
onFileTreeChange((fileTree) =>
updateFile(activePath, value, fileTree),
);
setDirty(true);
@@ -295,6 +295,78 @@ test("Preserves the currently open file path when building a template version",
expect(router.state.location.search).toBe("?path=myfile.tf");
});
test("Creating a new file opens it in the editor", async () => {
const user = userEvent.setup();
const { router } = renderWithAuth(<TemplateVersionEditorPage />, {
route: `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`,
path: "/templates/:template/versions/:version/edit",
});
// Wait for the default entrypoint file to load.
const editor = await screen.findByTestId("monaco-editor");
await waitFor(() => {
expect(editor).not.toHaveValue("");
});
const createButton = await screen.findByRole("button", {
name: "Create File",
});
await user.click(createButton);
const dialog = await screen.findByTestId("dialog");
const pathField = within(dialog).getByLabelText("File Path");
await user.type(pathField, "newfile.tf");
await user.click(within(dialog).getByRole("button", { name: "Create" }));
// The new (empty) file should be opened in the editor and the URL path
// query parameter should reflect the new file.
await waitFor(() => {
expect(screen.getByTestId("monaco-editor")).toHaveValue("");
});
expect(router.state.location.search).toBe("?path=newfile.tf");
});
test("Renaming a file does not throw and opens the new path", async () => {
const user = userEvent.setup();
const { router } = renderWithAuth(<TemplateVersionEditorPage />, {
route: `/templates/${MockTemplate.name}/versions/${MockTemplateVersion.name}/edit`,
path: "/templates/:template/versions/:version/edit",
});
// Wait for the default entrypoint file to load and capture its content so
// we can confirm the same content is shown after renaming.
const editor = await screen.findByTestId("monaco-editor");
await waitFor(() => {
expect(editor).not.toHaveValue("");
});
if (!(editor instanceof HTMLTextAreaElement)) {
throw new Error("editor is not a textarea");
}
const originalContent = editor.value;
// Open the file actions menu for the active file and click Rename.
const fileActions = await screen.findByRole("button", {
name: "File actions",
});
await user.click(fileActions);
await user.click(await screen.findByRole("menuitem", { name: /rename/i }));
const dialog = await screen.findByTestId("dialog");
const pathField = within(dialog).getByLabelText("File Path");
await user.clear(pathField);
await user.type(pathField, "renamed.tf");
await user.click(within(dialog).getByRole("button", { name: "Rename" }));
// The renamed file should still be open with its original content and
// the URL path query parameter should reflect the new name. Previously
// this path threw "File is not a text file" because the parent's stale
// file tree fell back to the old entrypoint name.
await waitFor(() => {
expect(screen.getByTestId("monaco-editor")).toHaveValue(originalContent);
});
expect(router.state.location.search).toBe("?path=renamed.tf");
});
describe.each([
{
testName: "Do not ask when template version has no errors",
@@ -72,7 +72,7 @@ const TemplateVersionEditorPage: FC = () => {
const logs = useWatchVersionLogs(activeTemplateVersion, {
onDone: activeTemplateVersionQuery.refetch,
});
const { fileTree, tarFile } = useFileTree(activeTemplateVersion);
const { fileTree, setFileTree, tarFile } = useFileTree(activeTemplateVersion);
const {
missingVariables,
setIsMissingVariablesDialogOpen,
@@ -138,7 +138,10 @@ const TemplateVersionEditorPage: FC = () => {
onActivePathChange={onActivePathChange}
template={templateQuery.data}
templateVersion={activeTemplateVersion}
defaultFileTree={fileTree}
fileTree={fileTree}
onFileTreeChange={(updater) => {
setFileTree((current) => (current ? updater(current) : current));
}}
onPreview={async (newFileTree) => {
if (!tarFile) {
return;
@@ -244,13 +247,8 @@ const useFileTree = (templateVersion: TemplateVersion | undefined) => {
...file(templateVersion?.job.file_id ?? ""),
enabled: templateVersion !== undefined,
});
const [state, setState] = useState<{
fileTree?: FileTree;
tarFile?: TarReader;
}>({
fileTree: undefined,
tarFile: undefined,
});
const [fileTree, setFileTree] = useState<FileTree | undefined>(undefined);
const [tarFile, setTarFile] = useState<TarReader | undefined>(undefined);
useEffect(() => {
let stale = false;
@@ -262,8 +260,8 @@ const useFileTree = (templateVersion: TemplateVersion | undefined) => {
if (stale) {
return;
}
const fileTree = createTemplateVersionFileTree(tarFile);
setState({ fileTree, tarFile });
setFileTree(createTemplateVersionFileTree(tarFile));
setTarFile(tarFile);
} catch (error) {
console.error(error);
toast.error("Error on initializing the editor.", {
@@ -281,7 +279,7 @@ const useFileTree = (templateVersion: TemplateVersion | undefined) => {
};
}, [fileQuery.data]);
return state;
return { fileTree, setFileTree, tarFile };
};
const useMissingVariables = (templateVersion: TemplateVersion | undefined) => {