mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): keep TemplateVersionEditor file tree in sync (#25068)
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user