From 8539692e32bc54a29d99d3fc44ee5b6d45a19f63 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 8 May 2025 11:39:08 +0000 Subject: [PATCH 1/4] feat: add tag select component for dynamic params --- .../RichParameterInput/MultiTextField.tsx | 61 ++++++------------- .../DynamicParameter/DynamicParameter.tsx | 49 ++++++++++----- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/site/src/components/RichParameterInput/MultiTextField.tsx b/site/src/components/RichParameterInput/MultiTextField.tsx index aed995299dbf3..9704804cee169 100644 --- a/site/src/components/RichParameterInput/MultiTextField.tsx +++ b/site/src/components/RichParameterInput/MultiTextField.tsx @@ -1,7 +1,6 @@ -import type { Interpolation, Theme } from "@emotion/react"; import Chip from "@mui/material/Chip"; import FormHelperText from "@mui/material/FormHelperText"; -import type { FC } from "react"; +import { type FC, useId, useMemo } from "react"; export type MultiTextFieldProps = { label: string; @@ -16,12 +15,25 @@ export const MultiTextField: FC = ({ values, onChange, }) => { + const baseId = useId(); + + const itemIds = useMemo(() => { + return Array.from( + { length: values.length }, + (_, index) => `${baseId}-item-${index}`, + ); + }, [baseId, values.length]); + return (
-
); }; - -const styles = { - root: (theme) => ({ - border: `1px solid ${theme.palette.divider}`, - borderRadius: 8, - minHeight: 48, // Chip height + paddings - padding: "10px 14px", - fontSize: 16, - display: "flex", - flexWrap: "wrap", - gap: 8, - position: "relative", - margin: "8px 0 4px", // Have same margin than TextField - - "&:has(input:focus)": { - borderColor: theme.palette.primary.main, - borderWidth: 2, - // Compensate for the border width - top: -1, - left: -1, - }, - }), - - input: { - flexGrow: 1, - fontSize: "inherit", - padding: 0, - border: "none", - background: "none", - - "&:focus": { - outline: "none", - }, - }, -} satisfies Record>; diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 9ec69158c4e84..2fd8d48a4e6f6 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -15,6 +15,7 @@ import { type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup"; +import { MultiTextField } from "components/RichParameterInput/MultiTextField"; import { Select, SelectContent, @@ -198,21 +199,7 @@ const ParameterField: FC = ({ ); case "multi-select": { - let values: string[] = []; - - if (value) { - try { - const parsed = JSON.parse(value); - if (Array.isArray(parsed)) { - values = parsed; - } - } catch (e) { - console.error( - "Error parsing parameter value with form_type multi-select", - e, - ); - } - } + const values = parseStringArrayValue(value); // Map parameter options to MultiSelectCombobox options format const options: Option[] = parameter.options.map((opt) => ({ @@ -259,6 +246,21 @@ const ParameterField: FC = ({ ); } + case "tag-select": { + const values = parseStringArrayValue(value); + + return ( + { + onChange(JSON.stringify(values)); + }} + /> + ); + } + case "switch": return ( = ({ } }; +const parseStringArrayValue = (value: string): string[] => { + let values: string[] = []; + + if (value) { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + values = parsed; + } + } catch (e) { + console.error("Error parsing parameter of type list(string)", e); + } + } + + return values; +}; + interface OptionDisplayProps { option: PreviewParameterOption; } From 3b430bcf762c5d8bb1f3dddb86b4290234497926 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 8 May 2025 11:44:18 +0000 Subject: [PATCH 2/4] chore: rename MultiTextField to TagInput --- site/src/components/RichParameterInput/RichParameterInput.tsx | 4 ++-- .../MultiTextField.tsx => TagInput/TagInput.tsx} | 4 ++-- .../modules/workspaces/DynamicParameter/DynamicParameter.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename site/src/components/{RichParameterInput/MultiTextField.tsx => TagInput/TagInput.tsx} (95%) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index beaff8ca2772e..84fe47bbbfee3 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -19,7 +19,7 @@ import type { AutofillBuildParameter, AutofillSource, } from "utils/richParameters"; -import { MultiTextField } from "./MultiTextField"; +import { TagInput } from "../TagInput/TagInput"; const isBoolean = (parameter: TemplateVersionParameter) => { return parameter.type === "bool"; @@ -372,7 +372,7 @@ const RichParameterField: FC = ({ } return ( - void; }; -export const MultiTextField: FC = ({ +export const TagInput: FC = ({ label, id, values, diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 2fd8d48a4e6f6..dd1f52d73b07d 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -15,7 +15,6 @@ import { type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup"; -import { MultiTextField } from "components/RichParameterInput/MultiTextField"; import { Select, SelectContent, @@ -25,6 +24,7 @@ import { } from "components/Select/Select"; import { Slider } from "components/Slider/Slider"; import { Switch } from "components/Switch/Switch"; +import { TagInput } from "components/TagInput/TagInput"; import { Textarea } from "components/Textarea/Textarea"; import { Tooltip, @@ -250,7 +250,7 @@ const ParameterField: FC = ({ const values = parseStringArrayValue(value); return ( - Date: Thu, 8 May 2025 11:52:36 +0000 Subject: [PATCH 3/4] chore: add TagInput stories --- .../components/TagInput/TagInput.stories.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 site/src/components/TagInput/TagInput.stories.tsx diff --git a/site/src/components/TagInput/TagInput.stories.tsx b/site/src/components/TagInput/TagInput.stories.tsx new file mode 100644 index 0000000000000..7b5e8285b8d64 --- /dev/null +++ b/site/src/components/TagInput/TagInput.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { TagInput } from "./TagInput"; + +const meta: Meta = { + title: "components/TagInput", + component: TagInput, + decorators: [(Story) =>
{Story()}
], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + values: [], + }, +}; + +export const WithEmptyTags: Story = { + args: { + values: ["", "", ""], + }, +}; + +export const WithLongTags: Story = { + args: { + values: ["this-is-a-very-long-long-long-tag-that-might-wrap", "another-long-tag-example", "short"], + }, +}; + +export const WithManyTags: Story = { + args: { + values: [ + "tag1", + "tag2", + "tag3", + "tag4", + "tag5", + "tag6", + "tag7", + "tag8", + "tag9", + "tag10", + "tag11", + "tag12", + "tag13", + "tag14", + "tag15", + "tag16", + "tag17", + "tag18", + "tag19", + "tag20", + ], + }, +}; From 7648fb6deb809ffdbc5fb4053e22714881790e0c Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 8 May 2025 12:01:33 +0000 Subject: [PATCH 4/4] fix: format --- .../components/TagInput/TagInput.stories.tsx | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/site/src/components/TagInput/TagInput.stories.tsx b/site/src/components/TagInput/TagInput.stories.tsx index 7b5e8285b8d64..5b1a9f8b14229 100644 --- a/site/src/components/TagInput/TagInput.stories.tsx +++ b/site/src/components/TagInput/TagInput.stories.tsx @@ -2,55 +2,59 @@ import type { Meta, StoryObj } from "@storybook/react"; import { TagInput } from "./TagInput"; const meta: Meta = { - title: "components/TagInput", - component: TagInput, - decorators: [(Story) =>
{Story()}
], + title: "components/TagInput", + component: TagInput, + decorators: [(Story) =>
{Story()}
], }; export default meta; type Story = StoryObj; export const Default: Story = { - args: { - values: [], - }, + args: { + values: [], + }, }; export const WithEmptyTags: Story = { - args: { - values: ["", "", ""], - }, + args: { + values: ["", "", ""], + }, }; export const WithLongTags: Story = { - args: { - values: ["this-is-a-very-long-long-long-tag-that-might-wrap", "another-long-tag-example", "short"], - }, + args: { + values: [ + "this-is-a-very-long-long-long-tag-that-might-wrap", + "another-long-tag-example", + "short", + ], + }, }; export const WithManyTags: Story = { - args: { - values: [ - "tag1", - "tag2", - "tag3", - "tag4", - "tag5", - "tag6", - "tag7", - "tag8", - "tag9", - "tag10", - "tag11", - "tag12", - "tag13", - "tag14", - "tag15", - "tag16", - "tag17", - "tag18", - "tag19", - "tag20", - ], - }, + args: { + values: [ + "tag1", + "tag2", + "tag3", + "tag4", + "tag5", + "tag6", + "tag7", + "tag8", + "tag9", + "tag10", + "tag11", + "tag12", + "tag13", + "tag14", + "tag15", + "tag16", + "tag17", + "tag18", + "tag19", + "tag20", + ], + }, };