feat: Add emoji picker for template icons (#3601)
|
@ -9,6 +9,7 @@ MacOS = "macOS"
|
|||
[files]
|
||||
extend-exclude = [
|
||||
"**.svg",
|
||||
"**.png",
|
||||
"**.lock",
|
||||
"go.sum",
|
||||
"go.mod",
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
declare module "can-ndjson-stream" {
|
||||
function ndjsonStream<TValueType>(
|
||||
body: ReadableStream<Uint8Array> | null,
|
||||
): Promise<ReadableStream<TValueType>>
|
||||
export default ndjsonStream
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
declare module "@emoji-mart/react" {
|
||||
const Picker: React.FC<{
|
||||
theme: "dark" | "light"
|
||||
data: Record<string, unknown>
|
||||
onEmojiSelect: (emojiData: { unified: string }) => void
|
||||
}>
|
||||
|
||||
export default Picker
|
||||
}
|
|
@ -57,7 +57,10 @@
|
|||
"xterm-addon-web-links": "0.6.0",
|
||||
"xterm-addon-webgl": "0.11.4",
|
||||
"xterm-for-react": "1.0.4",
|
||||
"yup": "0.32.11"
|
||||
"yup": "0.32.11",
|
||||
"@emoji-mart/data": "^1.0.5",
|
||||
"@emoji-mart/react": "^1.0.1",
|
||||
"emoji-mart": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.24.1",
|
||||
|
|
|
@ -75,7 +75,13 @@ export const ErrorSummary: FC<ErrorSummaryProps> = ({
|
|||
</Collapse>
|
||||
{retry && (
|
||||
<div className={styles.retry}>
|
||||
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
|
||||
<Button
|
||||
size="small"
|
||||
onClick={retry}
|
||||
startIcon={<RefreshIcon />}
|
||||
variant="outlined"
|
||||
className={styles.retryButton}
|
||||
>
|
||||
{Language.retryMessage}
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -122,4 +128,12 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
|||
retry: {
|
||||
marginTop: `${theme.spacing(2)}px`,
|
||||
},
|
||||
retryButton: {
|
||||
color: theme.palette.error.contrastText,
|
||||
borderColor: theme.palette.error.contrastText,
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import data from "@emoji-mart/data/sets/14/twitter.json"
|
||||
import Picker from "@emoji-mart/react"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { Template, UpdateTemplateMeta } from "api/typesGenerated"
|
||||
import { OpenDropdown } from "components/DropdownArrows/DropdownArrows"
|
||||
import { FormFooter } from "components/FormFooter/FormFooter"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import { FC, useRef, useState } from "react"
|
||||
import { colors } from "theme/colors"
|
||||
import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "util/formUtils"
|
||||
import * as Yup from "yup"
|
||||
|
||||
|
@ -17,6 +23,7 @@ export const Language = {
|
|||
// This is the same from the CLI on https://github.com/coder/coder/blob/546157b63ef9204658acf58cb653aa9936b70c49/cli/templateedit.go#L59
|
||||
maxTtlHelperText: "Edit the template maximum time before shutdown in milliseconds",
|
||||
formAriaLabel: "Template settings form",
|
||||
selectEmoji: "Select emoji",
|
||||
}
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
|
@ -43,6 +50,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
isSubmitting,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false)
|
||||
const form: FormikContextType<UpdateTemplateMeta> = useFormik<UpdateTemplateMeta>({
|
||||
initialValues: {
|
||||
name: template.name,
|
||||
|
@ -59,6 +67,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
const getFieldHelpers = getFormHelpersWithError<UpdateTemplateMeta>(form, error)
|
||||
const styles = useStyles()
|
||||
const hasIcon = form.values.icon && form.values.icon !== ""
|
||||
const emojiButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit} aria-label={Language.formAriaLabel}>
|
||||
|
@ -83,28 +92,61 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
rows={2}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("icon")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
label={Language.iconLabel}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: hasIcon ? (
|
||||
<InputAdornment position="end">
|
||||
<img
|
||||
alt=""
|
||||
src={form.values.icon}
|
||||
className={styles.adornment}
|
||||
// This prevent browser to display the ugly error icon if the
|
||||
// image path is wrong or user didn't finish typing the url
|
||||
onError={(e) => (e.currentTarget.style.display = "none")}
|
||||
onLoad={(e) => (e.currentTarget.style.display = "inline")}
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.iconField}>
|
||||
<TextField
|
||||
{...getFieldHelpers("icon")}
|
||||
disabled={isSubmitting}
|
||||
fullWidth
|
||||
label={Language.iconLabel}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
endAdornment: hasIcon ? (
|
||||
<InputAdornment position="end">
|
||||
<img
|
||||
alt=""
|
||||
src={form.values.icon}
|
||||
className={styles.adornment}
|
||||
// This prevent browser to display the ugly error icon if the
|
||||
// image path is wrong or user didn't finish typing the url
|
||||
onError={(e) => (e.currentTarget.style.display = "none")}
|
||||
onLoad={(e) => (e.currentTarget.style.display = "inline")}
|
||||
/>
|
||||
</InputAdornment>
|
||||
) : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
ref={emojiButtonRef}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
endIcon={<OpenDropdown />}
|
||||
onClick={() => {
|
||||
setIsEmojiPickerOpen((v) => !v)
|
||||
}}
|
||||
>
|
||||
{Language.selectEmoji}
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
id="emoji"
|
||||
open={isEmojiPickerOpen}
|
||||
anchorEl={emojiButtonRef.current}
|
||||
onClose={() => {
|
||||
setIsEmojiPickerOpen(false)
|
||||
}}
|
||||
>
|
||||
<Picker
|
||||
theme="dark"
|
||||
data={data}
|
||||
onEmojiSelect={(emojiData) => {
|
||||
form.setFieldValue("icon", `/emojis/${emojiData.unified}.png`)
|
||||
setIsEmojiPickerOpen(false)
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
{...getFieldHelpers("max_ttl_ms")}
|
||||
|
@ -123,8 +165,18 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
"@global": {
|
||||
"em-emoji-picker": {
|
||||
"--rgb-background": theme.palette.background.paper,
|
||||
"--rgb-input": colors.gray[17],
|
||||
"--rgb-color": colors.gray[4],
|
||||
},
|
||||
},
|
||||
adornment: {
|
||||
width: theme.spacing(3),
|
||||
height: theme.spacing(3),
|
||||
},
|
||||
iconField: {
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
},
|
||||
}))
|
||||
|
|
|
@ -9,7 +9,7 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => {
|
|||
MuiCssBaseline: {
|
||||
"@global": {
|
||||
body: {
|
||||
backgroundImage: `linear-gradient(to right bottom, ${colors.gray[15]}, ${colors.gray[17]})`,
|
||||
backgroundImage: `linear-gradient(to right bottom, ${palette.background.default}, ${colors.gray[17]})`,
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundAttachment: "fixed",
|
||||
letterSpacing: "-0.015em",
|
||||
|
@ -57,6 +57,12 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => {
|
|||
marginLeft: "0 !important",
|
||||
marginRight: 12,
|
||||
},
|
||||
outlined: {
|
||||
border: `1px solid ${palette.divider}`,
|
||||
"&:hover": {
|
||||
backgroundColor: palette.background.default,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
sizeSmall: {
|
||||
|
@ -82,8 +88,8 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => {
|
|||
root: {
|
||||
borderCollapse: "collapse",
|
||||
border: "none",
|
||||
background: colors.gray[15],
|
||||
boxShadow: `0 0 0 1px ${colors.gray[15]} inset`,
|
||||
background: palette.background.default,
|
||||
boxShadow: `0 0 0 1px ${palette.background.default} inset`,
|
||||
overflow: "hidden",
|
||||
|
||||
"& td": {
|
||||
|
|
After Width: | Height: | Size: 551 B |
After Width: | Height: | Size: 923 B |
After Width: | Height: | Size: 557 B |
After Width: | Height: | Size: 458 B |
After Width: | Height: | Size: 562 B |
After Width: | Height: | Size: 403 B |
After Width: | Height: | Size: 682 B |
After Width: | Height: | Size: 561 B |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 517 B |
After Width: | Height: | Size: 495 B |
After Width: | Height: | Size: 668 B |
After Width: | Height: | Size: 670 B |
After Width: | Height: | Size: 640 B |
After Width: | Height: | Size: 722 B |
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 747 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 697 B |
After Width: | Height: | Size: 287 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 849 B |
After Width: | Height: | Size: 989 B |
After Width: | Height: | Size: 662 B |
After Width: | Height: | Size: 245 B |
After Width: | Height: | Size: 723 B |
After Width: | Height: | Size: 481 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 847 B |
After Width: | Height: | Size: 205 B |
After Width: | Height: | Size: 835 B |
After Width: | Height: | Size: 387 B |
After Width: | Height: | Size: 230 B |
After Width: | Height: | Size: 402 B |
After Width: | Height: | Size: 559 B |
After Width: | Height: | Size: 652 B |
After Width: | Height: | Size: 406 B |
After Width: | Height: | Size: 336 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 390 B |
After Width: | Height: | Size: 244 B |
After Width: | Height: | Size: 411 B |
After Width: | Height: | Size: 940 B |
After Width: | Height: | Size: 279 B |
After Width: | Height: | Size: 978 B |
After Width: | Height: | Size: 919 B |
After Width: | Height: | Size: 884 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 871 B |
After Width: | Height: | Size: 805 B |
After Width: | Height: | Size: 419 B |
After Width: | Height: | Size: 1000 B |
After Width: | Height: | Size: 270 B |
After Width: | Height: | Size: 214 B |
After Width: | Height: | Size: 669 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 459 B |
After Width: | Height: | Size: 445 B |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 602 B |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 356 B |
After Width: | Height: | Size: 220 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 961 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 341 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 245 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 569 B |
After Width: | Height: | Size: 574 B |
After Width: | Height: | Size: 357 B |
After Width: | Height: | Size: 913 B |
After Width: | Height: | Size: 539 B |
After Width: | Height: | Size: 549 B |
After Width: | Height: | Size: 503 B |
After Width: | Height: | Size: 246 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 524 B |
After Width: | Height: | Size: 454 B |
After Width: | Height: | Size: 362 B |
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 461 B |
After Width: | Height: | Size: 586 B |
After Width: | Height: | Size: 809 B |
After Width: | Height: | Size: 362 B |