diff --git a/src/client/App.tsx b/src/client/App.tsx index d440302..1587375 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -178,14 +178,16 @@ export const App = () => { rel="noreferrer" className="font-light text-content-secondary text-sm hover:text-content-primary" > + (link opens in new tab) Coder + (link opens in new tab) Docs @@ -195,7 +197,7 @@ export const App = () => { {/* EDITOR */} - + @@ -325,6 +327,7 @@ const ExampleSelector: FC = () => { return ( + (link opens in new tab) {title} diff --git a/src/client/Editor.tsx b/src/client/Editor.tsx index 87c5fc8..09eb5e6 100644 --- a/src/client/Editor.tsx +++ b/src/client/Editor.tsx @@ -1,3 +1,13 @@ +import { Editor as MonacoEditor } from "@monaco-editor/react"; +import { + CheckIcon, + ChevronDownIcon, + CopyIcon, + FileJsonIcon, + SettingsIcon, + ZapIcon, +} from "lucide-react"; +import { type FC, useEffect, useState } from "react"; import { Button } from "@/client/components/Button"; import { DropdownMenu, @@ -13,57 +23,38 @@ import { TooltipContent, TooltipTrigger, } from "@/client/components/Tooltip"; +import { useEditor } from "@/client/contexts/editor"; import { useTheme } from "@/client/contexts/theme"; -import { multiSelect, radio, switchInput, textInput } from "@/client/snippets"; -import type { ParameterFormType } from "@/gen/types"; +import { type SnippetFunc, snippets } from "@/client/snippets"; +import type { ParameterWithSource } from "@/gen/types"; import { cn } from "@/utils/cn"; -import { Editor as MonacoEditor } from "@monaco-editor/react"; -import { - CheckIcon, - ChevronDownIcon, - CopyIcon, - FileJsonIcon, - RadioIcon, - SettingsIcon, - SquareMousePointerIcon, - TextCursorInputIcon, - ToggleLeftIcon, - ZapIcon, -} from "lucide-react"; -import { type FC, useEffect, useRef, useState } from "react"; -import { useEditor } from "@/client/contexts/editor"; type EditorProps = { code: string; setCode: React.Dispatch>; + parameters: ParameterWithSource[]; }; -export const Editor: FC = ({ code, setCode }) => { +export const Editor: FC = ({ code, setCode, parameters }) => { const { appliedTheme } = useTheme(); const editorRef = useEditor(); - const [codeCopied, setCodeCopied] = useState(() => false); - const copyTimeoutId = useRef | undefined>( - undefined, - ); - const [tab, setTab] = useState(() => "code"); + const [codeCopied, setCodeCopied] = useState(() => false); + const onCopy = () => { navigator.clipboard.writeText(code); setCodeCopied(() => true); }; - const onAddSnippet = (formType: ParameterFormType) => { - if (formType === "input") { - setCode(`${code.trimEnd()}\n\n${textInput}\n`); - } else if (formType === "radio") { - setCode(`${code.trimEnd()}\n\n${radio}\n`); - } else if (formType === "multi-select") { - setCode(`${code.trimEnd()}\n\n${multiSelect}\n`); - } else if (formType === "switch") { - setCode(`${code.trimEnd()}\n\n${switchInput}\n`); - } + const onAddSnippet = (name: string, snippet: SnippetFunc) => { + const nameCount = parameters.filter((p) => p.name.startsWith(name)).length; + + const nextInOrder = 1 + Math.max(0, ...parameters.map((p) => p.order)); + const newName = nameCount > 0 ? `${name}-${nameCount}` : name; + const newSnippet = snippet(newName, nextInOrder); + setCode(`${code.trimEnd()}\n\n${newSnippet}\n`); }; useEffect(() => { @@ -71,13 +62,11 @@ export const Editor: FC = ({ code, setCode }) => { return; } - clearTimeout(copyTimeoutId.current); - - copyTimeoutId.current = setTimeout(() => { + const copyTimeoutId = setTimeout(() => { setCodeCopied(() => false); }, 1000); - return () => clearTimeout(copyTimeoutId.current); + return () => clearTimeout(copyTimeoutId); }, [codeCopied]); return ( @@ -116,23 +105,17 @@ export const Editor: FC = ({ code, setCode }) => { - onAddSnippet("input")}> - - Text input - - onAddSnippet("multi-select")} - > - - Multi-select - - onAddSnippet("radio")}> - - Radio - - onAddSnippet("switch")}> - Switches - + {snippets.map( + ({ name, label, icon: Icon, snippet }, index) => ( + onAddSnippet(name, snippet)} + > + + {label} + + ), + )} diff --git a/src/client/Preview.tsx b/src/client/Preview.tsx index 9ca2d27..f40c6eb 100644 --- a/src/client/Preview.tsx +++ b/src/client/Preview.tsx @@ -239,9 +239,12 @@ const PreviewEmptyState = () => {

+ (link opens in new tab) Read the docs diff --git a/src/client/snippets.ts b/src/client/snippets.ts index 26b8fef..f32c7e9 100644 --- a/src/client/snippets.ts +++ b/src/client/snippets.ts @@ -1,3 +1,15 @@ +import { + LetterTextIcon, + type LucideIcon, + RadioIcon, + Rows3Icon, + Settings2Icon, + SquareMousePointerIcon, + TagIcon, + TextCursorInputIcon, + ToggleLeftIcon, +} from "lucide-react"; + export const defaultCode = `terraform { required_providers { coder = { @@ -7,25 +19,104 @@ export const defaultCode = `terraform { } }`; -export const textInput = `data "coder_parameter" "project-name" { - display_name = "An input" - name = "an-input" - description = "What is the name of your project?" - order = 4 +export type SnippetFunc = (name: string, order: number) => string; +type Snippet = { + name: string; + label: string; + icon: LucideIcon; + snippet: SnippetFunc; +}; + +export const input: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "text-input" { + name = "${name}" + display_name = "A text input" + description = "This parameter can be used to input text." + order = ${order} + + styling = jsonencode({ + placeholder = "A placeholder that will appear if the input value is empty" + }) form_type = "input" type = "string" default = "An input value" }`; -export const radio = `data "coder_parameter" "radio" { - name = "radio" - display_name = "An example of a radio input" - description = "The next parameter supports a single value." - type = "string" - form_type = "radio" - order = 1 - default = "option-1" +export const textarea: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "textarea" { + name = "${name}" + display_name = "A textarea input" + description = "This parameter can be used to input multiple lines of text" + order = ${order} + + styling = jsonencode({ + placeholder = "A placeholder that will appear if the input value is empty" + }) + + form_type = "textarea" + type = "string" + default = "An input value" +}`; + +export const radio: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "radio" { + name = "${name}" + display_name = "A radio input" + description = "This parameter supports selecting a single value out of a list of options" + order = ${order} + + type = "string" + form_type = "radio" + default = "option-1" + + option { + name = "Option 1" + value = "option-1" + description = "A description for Option 1" + } + + option { + name = "Option 2" + value = "option-2" + description = "A description for Option 2" + } + + option { + name = "Option 3" + value = "option-3" + description = "A description for Option 3" + } + + option { + name = "Option 4" + value = "option-4" + description = "A description for Option 4" + } +}`; + +export const dropdown: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "dropdown" { + name = "${name}" + display_name = "A dropdown input" + description = "This parameter supports selecting a single value out of a list of options. Especially useful when you have a lot of options." + order = ${order} + + styling = jsonencode({ + placeholder = "A placeholder that will appear if the input value is empty" + }) + + type = "string" + form_type = "dropdown" + default = "option-1" option { name = "Option 1" @@ -52,13 +143,17 @@ export const radio = `data "coder_parameter" "radio" { } }`; -export const multiSelect = `data "coder_parameter" "multi-select" { - name = "multi-select" - display_name = "An example of a multi-select" - description = "The next parameter supports multiple values." +export const multiSelect: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "multi-select" { + name = "${name}" + display_name = "A multi-select input" + description = "This parameter supports selecting multiple values from a list of options" + order = ${order} + type = "list(string)" form_type = "multi-select" - order = 1 option { name = "Option 1" @@ -85,15 +180,101 @@ export const multiSelect = `data "coder_parameter" "multi-select" { } }`; -export const switchInput = `data "coder_parameter" "switch" { - name = "switch" - display_name = "An example of a switch" - description = "The next parameter can be on or off" +export const tagSelect: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "tag-select" { + name = "${name}" + display_name = "A tag-select input" + description = "This parameter supports selecting multiple user inputed values at once" + order = ${order} + + type = "list(string)" + form_type = "tag-select" +}`; + +export const switchInput: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "switch" { + name = "${name}" + display_name = "A switch input" + description = "This parameter can be toggled between true and false" + order = ${order} + type = "bool" form_type = "switch" default = true - order = 1 -}` +}`; + +export const slider: SnippetFunc = ( + name, + order, +) => `data "coder_parameter" "slider" { + name = "${name}" + display_name = "A slider input" + description = "This parameter supports selecting a number within a given range" + type = "number" + form_type = "slider" + default = 6 + order = ${order} + + validation { + min = 1 + max = 10 + } +}`; + +export const snippets: Snippet[] = [ + { + name: "text-input", + label: "Text Input", + icon: TextCursorInputIcon, + snippet: input, + }, + { + name: "textarea", + label: "Textarea", + icon: LetterTextIcon, + snippet: textarea, + }, + { + name: "radio", + label: "Radio", + icon: RadioIcon, + snippet: radio, + }, + { + name: "switch", + label: "Multi-select", + icon: SquareMousePointerIcon, + snippet: multiSelect, + }, + { + name: "tag-select", + label: "Tag-select", + icon: TagIcon, + snippet: tagSelect, + }, + { + name: "switch", + label: "Switch", + icon: ToggleLeftIcon, + snippet: switchInput, + }, + { + name: "dropdown", + label: "Dropdown", + icon: Rows3Icon, + snippet: dropdown, + }, + { + name: "slider", + label: "Slider", + icon: Settings2Icon, + snippet: slider, + }, +]; export const checkerModule = ` variable "solutions" {