mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): Support list(string) rich parameter field (#6653)
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
import { Story } from "@storybook/react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { MultiTextField, MultiTextFieldProps } from "./MultiTextField"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "components/MultiTextField",
|
||||||
|
component: MultiTextField,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Template: Story<MultiTextFieldProps> = (args) => {
|
||||||
|
const [values, setValues] = useState(args.values ?? ["foo", "bar"])
|
||||||
|
return <MultiTextField {...args} values={values} onChange={setValues} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Example = Template.bind({})
|
||||||
|
Example.args = {}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import Chip from "@material-ui/core/Chip"
|
||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export type MultiTextFieldProps = {
|
||||||
|
label: string
|
||||||
|
values: string[]
|
||||||
|
onChange: (values: string[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiTextField: FC<MultiTextFieldProps> = ({
|
||||||
|
label,
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={styles.root}>
|
||||||
|
{values.map((value, index) => (
|
||||||
|
<Chip
|
||||||
|
key={index}
|
||||||
|
label={value}
|
||||||
|
size="small"
|
||||||
|
onDelete={() => {
|
||||||
|
onChange(values.filter((oldValue) => oldValue !== value))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
aria-label={label}
|
||||||
|
className={styles.input}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === ",") {
|
||||||
|
event.preventDefault()
|
||||||
|
const newValue = event.currentTarget.value
|
||||||
|
onChange([...values, newValue])
|
||||||
|
event.currentTarget.value = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "Backspace" && event.currentTarget.value === "") {
|
||||||
|
event.preventDefault()
|
||||||
|
const lastValue = values[values.length - 1]
|
||||||
|
onChange(values.slice(0, -1))
|
||||||
|
event.currentTarget.value = lastValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={(event) => {
|
||||||
|
if (event.currentTarget.value !== "") {
|
||||||
|
const newValue = event.currentTarget.value
|
||||||
|
onChange([...values, newValue])
|
||||||
|
event.currentTarget.value = ""
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
root: {
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
minHeight: theme.spacing(5),
|
||||||
|
padding: theme.spacing(1.25, 1.75),
|
||||||
|
fontSize: theme.spacing(2),
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
position: "relative",
|
||||||
|
margin: theme.spacing(1, 0, 0.5), // 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -31,6 +31,7 @@ const createTemplateVersionParameter = (
|
|||||||
validation_max: 0,
|
validation_max: 0,
|
||||||
validation_monotonic: "increasing",
|
validation_monotonic: "increasing",
|
||||||
description_plaintext: "",
|
description_plaintext: "",
|
||||||
|
required: true,
|
||||||
...partial,
|
...partial,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +100,17 @@ OptionsType.args = {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ListStringType = Template.bind({})
|
||||||
|
ListStringType.args = {
|
||||||
|
initialValue: JSON.stringify(["first", "second", "third"]),
|
||||||
|
id: "list_string_parameter",
|
||||||
|
parameter: createTemplateVersionParameter({
|
||||||
|
name: "list_string_parameter",
|
||||||
|
type: "list(string)",
|
||||||
|
description: "List string parameter",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
export const IconLabel = Template.bind({})
|
export const IconLabel = Template.bind({})
|
||||||
IconLabel.args = {
|
IconLabel.args = {
|
||||||
initialValue: "initial-value",
|
initialValue: "initial-value",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { FC, useState } from "react"
|
|||||||
import { TemplateVersionParameter } from "../../api/typesGenerated"
|
import { TemplateVersionParameter } from "../../api/typesGenerated"
|
||||||
import { colors } from "theme/colors"
|
import { colors } from "theme/colors"
|
||||||
import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
||||||
|
import { MultiTextField } from "components/MultiTextField/MultiTextField"
|
||||||
|
|
||||||
const isBoolean = (parameter: TemplateVersionParameter) => {
|
const isBoolean = (parameter: TemplateVersionParameter) => {
|
||||||
return parameter.type === "bool"
|
return parameter.type === "bool"
|
||||||
@@ -154,6 +155,34 @@ const RichParameterField: React.FC<RichParameterInputProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameter.type === "list(string)") {
|
||||||
|
let values: string[] = []
|
||||||
|
|
||||||
|
if (parameterValue) {
|
||||||
|
try {
|
||||||
|
values = JSON.parse(parameterValue) as string[]
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing list(string) parameter", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiTextField
|
||||||
|
label={props.label as string}
|
||||||
|
values={values}
|
||||||
|
onChange={(values) => {
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(values)
|
||||||
|
setParameterValue(value)
|
||||||
|
onChange(value)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error on change of list(string) parameter", e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// A text field can technically handle all cases!
|
// A text field can technically handle all cases!
|
||||||
// As other cases become more prominent (like filtering for numbers),
|
// As other cases become more prominent (like filtering for numbers),
|
||||||
// we should break this out into more finely scoped input fields.
|
// we should break this out into more finely scoped input fields.
|
||||||
|
|||||||
@@ -229,5 +229,10 @@ export const getOverrides = ({
|
|||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiChip: {
|
||||||
|
root: {
|
||||||
|
backgroundColor: colors.gray[12],
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user