Skip to content

Commit 4b1bac3

Browse files
feat(site): allow any file extension on template editor (#12000)
1 parent 4e7b208 commit 4b1bac3

20 files changed

+517
-333
lines changed

site/src/components/SyntaxHighlighter/SyntaxHighlighter.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import { type ComponentProps, type FC } from "react";
22
import Editor, { DiffEditor, loader } from "@monaco-editor/react";
33
import * as monaco from "monaco-editor";
44
import { useCoderTheme } from "./coderTheme";
5+
import { useTheme } from "@emotion/react";
56

67
loader.config({ monaco });
78

89
interface SyntaxHighlighterProps {
910
value: string;
10-
language: string;
11+
language?: string;
1112
editorProps?: ComponentProps<typeof Editor> &
1213
ComponentProps<typeof DiffEditor>;
1314
compareWith?: string;
@@ -20,6 +21,7 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
2021
editorProps,
2122
}) => {
2223
const hasDiff = compareWith && value !== compareWith;
24+
const theme = useTheme();
2325
const coderTheme = useCoderTheme();
2426
const commonProps = {
2527
language,
@@ -45,6 +47,7 @@ export const SyntaxHighlighter: FC<SyntaxHighlighterProps> = ({
4547
css={{
4648
padding: "8px 0",
4749
height: "100%",
50+
backgroundColor: theme.monaco.colors["editor.background"],
4851
}}
4952
>
5053
{hasDiff ? (
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
3+
import { TemplateFileTree } from "./TemplateFileTree";
4+
import { FileTree } from "utils/filetree";
5+
import { useTheme } from "@emotion/react";
6+
7+
const fileTree: FileTree = {
8+
"main.tf": "resource aws_instance my_instance {}",
9+
"variables.tf": "variable my_var {}",
10+
"outputs.tf": "output my_output {}",
11+
folder: {
12+
"nested.tf": "resource aws_instance my_instance {}",
13+
},
14+
};
15+
16+
const meta: Meta<typeof TemplateFileTree> = {
17+
title: "modules/templates/TemplateFileTree",
18+
parameters: { chromatic },
19+
component: TemplateFileTree,
20+
args: {
21+
fileTree,
22+
activePath: "main.tf",
23+
},
24+
decorators: [
25+
(Story) => {
26+
const theme = useTheme();
27+
return (
28+
<div
29+
css={{
30+
maxWidth: 260,
31+
borderRadius: 8,
32+
border: `1px solid ${theme.palette.divider}`,
33+
}}
34+
>
35+
<Story />
36+
</div>
37+
);
38+
},
39+
],
40+
};
41+
42+
export default meta;
43+
type Story = StoryObj<typeof TemplateFileTree>;
44+
45+
export const Example: Story = {};
46+
47+
export const NestedOpen: Story = {
48+
args: {
49+
activePath: "folder/nested.tf",
50+
},
51+
};
52+
53+
export const GroupEmptyFolders: Story = {
54+
args: {
55+
activePath: "folder/other-folder/another/nested.tf",
56+
fileTree: {
57+
"main.tf": "resource aws_instance my_instance {}",
58+
"variables.tf": "variable my_var {}",
59+
"outputs.tf": "output my_output {}",
60+
folder: {
61+
"other-folder": {
62+
another: {
63+
"nested.tf": "resource aws_instance my_instance {}",
64+
},
65+
},
66+
},
67+
},
68+
},
69+
};
70+
71+
export const GreyOutHiddenFiles: Story = {
72+
args: {
73+
fileTree: {
74+
".vite": {
75+
"config.json": "resource aws_instance my_instance {}",
76+
},
77+
".nextjs": {
78+
"nested.tf": "resource aws_instance my_instance {}",
79+
},
80+
".terraform.lock.hcl": "{}",
81+
"main.tf": "resource aws_instance my_instance {}",
82+
"variables.tf": "variable my_var {}",
83+
"outputs.tf": "output my_output {}",
84+
},
85+
},
86+
};

site/src/pages/TemplateVersionEditorPage/FileTreeView.tsx renamed to site/src/modules/templates/TemplateFiles/TemplateFileTree.tsx

+73-15
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,59 @@ type ContextMenu = {
2828
clientY: number;
2929
};
3030

31-
interface FileTreeViewProps {
31+
interface TemplateFilesTreeProps {
3232
onSelect: (path: string) => void;
33-
onDelete: (path: string) => void;
34-
onRename: (path: string) => void;
33+
onDelete?: (path: string) => void;
34+
onRename?: (path: string) => void;
3535
fileTree: FileTree;
3636
activePath?: string;
37+
Label?: FC<{
38+
path: string;
39+
filename: string;
40+
label: string;
41+
isFolder: boolean;
42+
}>;
3743
}
3844

39-
export const FileTreeView: FC<FileTreeViewProps> = ({
45+
export const TemplateFileTree: FC<TemplateFilesTreeProps> = ({
4046
fileTree,
4147
activePath,
4248
onDelete,
4349
onRename,
4450
onSelect,
51+
Label,
4552
}) => {
4653
const [contextMenu, setContextMenu] = useState<ContextMenu | undefined>();
54+
55+
const isFolder = (content?: FileTree | string): content is FileTree =>
56+
typeof content === "object";
57+
4758
const buildTreeItems = (
59+
label: string,
4860
filename: string,
4961
content?: FileTree | string,
5062
parentPath?: string,
5163
): JSX.Element => {
5264
const currentPath = parentPath ? `${parentPath}/${filename}` : filename;
53-
const isFolder = typeof content === "object";
54-
let icon: JSX.Element | null = isFolder ? null : (
65+
// Used to group empty folders in one single label like VSCode does
66+
const shouldGroupFolder =
67+
isFolder(content) &&
68+
Object.keys(content).length === 1 &&
69+
isFolder(Object.values(content)[0]);
70+
const isHiddenFile = currentPath.startsWith(".");
71+
72+
if (shouldGroupFolder) {
73+
const firstChildFileName = Object.keys(content)[0];
74+
const child = content[firstChildFileName];
75+
return buildTreeItems(
76+
`${label} / ${firstChildFileName}`,
77+
firstChildFileName,
78+
child,
79+
currentPath,
80+
);
81+
}
82+
83+
let icon: JSX.Element | null = isFolder(content) ? null : (
5584
<FormatAlignLeftOutlined />
5685
);
5786

@@ -69,26 +98,40 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
6998
<TreeItem
7099
nodeId={currentPath}
71100
key={currentPath}
72-
label={filename}
101+
label={
102+
Label ? (
103+
<Label
104+
path={currentPath}
105+
label={label}
106+
filename={filename}
107+
isFolder={isFolder(content)}
108+
/>
109+
) : (
110+
label
111+
)
112+
}
73113
css={(theme) => css`
74114
overflow: hidden;
75115
user-select: none;
76116
77117
& > .MuiTreeItem-content {
78118
padding: 2px 16px;
79-
color: ${theme.palette.text.secondary};
119+
color: ${isHiddenFile
120+
? theme.palette.text.disabled
121+
: theme.palette.text.secondary};
80122
height: 32px;
81123
82124
& svg {
83125
width: 12px;
84126
height: 12px;
85-
color: ${theme.palette.text.secondary};
127+
color: currentColor;
86128
}
87129
88130
& > .MuiTreeItem-label {
89131
margin-left: 4px;
90132
font-size: 13px;
91133
color: inherit;
134+
white-space: nowrap;
92135
}
93136
94137
&.Mui-selected {
@@ -103,17 +146,22 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
103146
104147
& .MuiTreeItem-group {
105148
margin-left: 0;
149+
position: relative;
106150
107151
// We need to find a better way to recursive padding here
108152
& .MuiTreeItem-content {
109-
padding-left: calc(var(--level) * 40px);
153+
padding-left: calc(8px + (var(--level) + 1) * 8px);
110154
}
111155
}
112156
`}
113157
onClick={() => {
114158
onSelect(currentPath);
115159
}}
116160
onContextMenu={(event) => {
161+
const hasContextActions = onRename || onDelete;
162+
if (!hasContextActions) {
163+
return;
164+
}
117165
event.preventDefault(); // Avoid default browser behavior
118166
event.stopPropagation(); // Avoid trigger parent context menu
119167
setContextMenu(
@@ -133,12 +181,12 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
133181
} as CSSProperties
134182
}
135183
>
136-
{isFolder &&
184+
{isFolder(content) &&
137185
Object.keys(content)
138186
.sort(sortFileTree(content))
139187
.map((filename) => {
140188
const child = content[filename];
141-
return buildTreeItems(filename, child, currentPath);
189+
return buildTreeItems(filename, filename, child, currentPath);
142190
})}
143191
</TreeItem>
144192
);
@@ -149,13 +197,14 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
149197
defaultCollapseIcon={<ExpandMoreIcon />}
150198
defaultExpandIcon={<ChevronRightIcon />}
151199
aria-label="Files"
200+
defaultExpanded={activePath ? expandablePaths(activePath) : []}
152201
defaultSelected={activePath}
153202
>
154203
{Object.keys(fileTree)
155204
.sort(sortFileTree(fileTree))
156205
.map((filename) => {
157206
const child = fileTree[filename];
158-
return buildTreeItems(filename, child);
207+
return buildTreeItems(filename, filename, child);
159208
})}
160209

161210
<Menu
@@ -184,7 +233,7 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
184233
if (!contextMenu) {
185234
return;
186235
}
187-
onRename(contextMenu.path);
236+
onRename && onRename(contextMenu.path);
188237
setContextMenu(undefined);
189238
}}
190239
>
@@ -195,7 +244,7 @@ export const FileTreeView: FC<FileTreeViewProps> = ({
195244
if (!contextMenu) {
196245
return;
197246
}
198-
onDelete(contextMenu.path);
247+
onDelete && onDelete(contextMenu.path);
199248
setContextMenu(undefined);
200249
}}
201250
>
@@ -232,3 +281,12 @@ const FileTypeMarkdown: FC = () => (
232281
<polygon points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636" />
233282
</svg>
234283
);
284+
285+
const expandablePaths = (path: string) => {
286+
const paths = path.split("/");
287+
const result = [];
288+
for (let i = 1; i < paths.length; i++) {
289+
result.push(paths.slice(0, i).join("/"));
290+
}
291+
return result;
292+
};

site/src/modules/templates/TemplateFiles/TemplateFiles.stories.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { action } from "@storybook/addon-actions";
21
import type { Meta, StoryObj } from "@storybook/react";
32
import { chromatic } from "testHelpers/chromatic";
43
import { TemplateFiles } from "./TemplateFiles";
@@ -19,7 +18,6 @@ const meta: Meta<typeof TemplateFiles> = {
1918
args: {
2019
currentFiles: exampleFiles,
2120
baseFiles: exampleFiles,
22-
tab: { value: "0", set: action("change tab") },
2321
},
2422
};
2523

@@ -28,4 +26,14 @@ type Story = StoryObj<typeof TemplateFiles>;
2826

2927
const Example: Story = {};
3028

29+
export const WithDiff: Story = {
30+
args: {
31+
currentFiles: {
32+
...exampleFiles,
33+
"main.tf": `${exampleFiles["main.tf"]} - with changes`,
34+
},
35+
baseFiles: exampleFiles,
36+
},
37+
};
38+
3139
export { Example as TemplateFiles };

0 commit comments

Comments
 (0)