From 1b6fe0c8c711b7e4d0451fc380fc1e4f827803d6 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 3 Jan 2024 19:18:41 +0000 Subject: [PATCH 1/6] feat: select group avatars with the emoji picker --- site/src/@types/emoji-mart.d.ts | 8 +- site/src/components/IconField/EmojiPicker.tsx | 40 +++++++++ .../IconField/IconField.stories.tsx | 2 +- site/src/components/IconField/IconField.tsx | 87 +++++++++---------- .../components/IconField/LazyIconField.tsx | 11 --- .../CreateTemplatePage/CreateTemplateForm.tsx | 5 +- .../pages/GroupsPage/CreateGroupPageView.tsx | 8 +- .../GroupsPage/SettingsGroupPageView.tsx | 4 +- .../TemplateSettingsForm.tsx | 4 +- 9 files changed, 98 insertions(+), 71 deletions(-) create mode 100644 site/src/components/IconField/EmojiPicker.tsx delete mode 100644 site/src/components/IconField/LazyIconField.tsx diff --git a/site/src/@types/emoji-mart.d.ts b/site/src/@types/emoji-mart.d.ts index 18c1d81eabb0e..6d13bf6e2c2b1 100644 --- a/site/src/@types/emoji-mart.d.ts +++ b/site/src/@types/emoji-mart.d.ts @@ -28,7 +28,7 @@ declare module "@emoji-mart/react" { | { unified: undefined; src: string } | { unified: string; src: undefined }; - const EmojiPicker: React.FC<{ + export interface EmojiMartProps { set: "native" | "apple" | "facebook" | "google" | "twitter"; theme: "dark" | "light"; data: unknown; @@ -36,7 +36,9 @@ declare module "@emoji-mart/react" { emojiButtonSize?: number; emojiSize?: number; onEmojiSelect: (emoji: EmojiData) => void; - }>; + } + + const EmojiMart: React.FC; - export default EmojiPicker; + export default EmojiMart; } diff --git a/site/src/components/IconField/EmojiPicker.tsx b/site/src/components/IconField/EmojiPicker.tsx new file mode 100644 index 0000000000000..3b39f7b3ac519 --- /dev/null +++ b/site/src/components/IconField/EmojiPicker.tsx @@ -0,0 +1,40 @@ +import EmojiMart, { type EmojiMartProps } from "@emoji-mart/react"; +import data from "@emoji-mart/data/sets/14/twitter.json"; +import { type FC } from "react"; +import icons from "theme/icons.json"; + +const custom = [ + { + id: "icons", + name: "Icons", + emojis: icons.map((icon) => { + const id = icon.split(".")[0]; + + return { + id, + name: id, + keywords: id.split("-"), + skins: [{ src: `/icon/${icon}` }], + }; + }), + }, +]; + +type EmojiPickerProps = Omit< + EmojiMartProps, + "custom" | "data" | "set" | "theme" +>; + +const EmojiPicker: FC = (props) => { + return ( + + ); +}; + +export default EmojiPicker; diff --git a/site/src/components/IconField/IconField.stories.tsx b/site/src/components/IconField/IconField.stories.tsx index 35b558d02353c..2d4282a12cbea 100644 --- a/site/src/components/IconField/IconField.stories.tsx +++ b/site/src/components/IconField/IconField.stories.tsx @@ -1,6 +1,6 @@ import { action } from "@storybook/addon-actions"; -import IconField from "./IconField"; import type { Meta, StoryObj } from "@storybook/react"; +import { IconField } from "./IconField"; const meta: Meta = { title: "components/IconField", diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index ea578d07734b9..286ba89adb216 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -2,17 +2,16 @@ import { css, Global, useTheme } from "@emotion/react"; import Button from "@mui/material/Button"; import InputAdornment from "@mui/material/InputAdornment"; import TextField, { type TextFieldProps } from "@mui/material/TextField"; -import Picker from "@emoji-mart/react"; -import { type FC } from "react"; +import { type FC, lazy, Suspense } from "react"; +import { Loader } from "components/Loader/Loader"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack"; -import data from "@emoji-mart/data/sets/14/twitter.json"; -import icons from "theme/icons.json"; import { Popover, PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; +import { visuallyHidden } from "@mui/utils"; // See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222 const urlFromUnifiedCode = (unified: string) => @@ -22,24 +21,12 @@ type IconFieldProps = TextFieldProps & { onPickEmoji: (value: string) => void; }; -const custom = [ - { - id: "icons", - name: "Icons", - emojis: icons.map((icon) => { - const id = icon.split(".")[0]; +const EmojiPicker = lazy(() => import("./EmojiPicker")); - return { - id, - name: id, - keywords: id.split("-"), - skins: [{ src: `/icon/${icon}` }], - }; - }), - }, -]; - -const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { +export const IconField: FC = ({ + onPickEmoji, + ...textFieldProps +}) => { if ( typeof textFieldProps.value !== "string" && typeof textFieldProps.value !== "undefined" @@ -53,9 +40,9 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { return ( = ({ onPickEmoji, ...textFieldProps }) => { }} /> + {(popover) => ( <> @@ -98,35 +97,29 @@ const IconField: FC = ({ onPickEmoji, ...textFieldProps }) => { id="emoji" css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }} > - - { - const value = emoji.src ?? urlFromUnifiedCode(emoji.unified); - onPickEmoji(value); - popover.setIsOpen(false); - }} - /> + }> + { + const value = + emoji.src ?? urlFromUnifiedCode(emoji.unified); + onPickEmoji(value); + popover.setIsOpen(false); + }} + /> + )} + + {/* This component takes a long time to load (easily several seconds), so we + don't want to wait until the user actually clicks the button to start loading. + Unfortunately, React doesn't provide an API to start warming a lazy component, + so we just have to sneak it into the DOM, which is kind of annoying, but means + that users shouldn't ever spend time waiting for it to load. */} +
+ {}} /> +
); }; - -export default IconField; diff --git a/site/src/components/IconField/LazyIconField.tsx b/site/src/components/IconField/LazyIconField.tsx deleted file mode 100644 index be6b444f747be..0000000000000 --- a/site/src/components/IconField/LazyIconField.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { lazy, Suspense, type ComponentProps } from "react"; - -const IconField = lazy(() => import("./IconField")); - -export const LazyIconField = (props: ComponentProps) => { - return ( - }> - - - ); -}; diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index c87d9cefe3936..6c5bc538310d1 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -27,7 +27,7 @@ import { HelpTooltipText, HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; -import { LazyIconField } from "components/IconField/LazyIconField"; +import { IconField } from "components/IconField/IconField"; import Link from "@mui/material/Link"; import { HorizontalForm, @@ -345,12 +345,11 @@ export const CreateTemplateForm: FC = (props) => { label="Description" /> - form.setFieldValue("icon", value)} /> diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index 2459e2703547d..b3df5edce0049 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -2,6 +2,7 @@ import TextField from "@mui/material/TextField"; import { CreateGroupRequest } from "api/typesGenerated"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; +import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; import { useFormik } from "formik"; @@ -39,6 +40,7 @@ export const CreateGroupPageView: FC = ({ const getFieldHelpers = getFormHelpers(form, formErrors); const onCancel = () => navigate("/groups"); + console.log("i'll just cry then"); return ( @@ -58,12 +60,14 @@ export const CreateGroupPageView: FC = ({ fullWidth label="Display Name" /> - ( + console.log(value), form.setFieldValue("avatar_url", value) + )} /> diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx index b28a5bfba404f..34ddab4841470 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx @@ -3,7 +3,7 @@ import { Group } from "api/typesGenerated"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; import { Loader } from "components/Loader/Loader"; -import { LazyIconField } from "components/IconField/LazyIconField"; +import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { useFormik } from "formik"; import { FC } from "react"; @@ -84,7 +84,7 @@ const UpdateGroupForm: FC = ({ label="Display Name" disabled={isEveryoneGroup(group)} /> - = ({ rows={2} /> - Date: Wed, 3 Jan 2024 19:35:03 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/pages/GroupsPage/CreateGroupPageView.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index b3df5edce0049..798271861a002 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -40,7 +40,6 @@ export const CreateGroupPageView: FC = ({ const getFieldHelpers = getFormHelpers(form, formErrors); const onCancel = () => navigate("/groups"); - console.log("i'll just cry then"); return ( @@ -65,9 +64,7 @@ export const CreateGroupPageView: FC = ({ onChange={onChangeTrimmed(form)} fullWidth label="Avatar URL" - onPickEmoji={(value) => ( - console.log(value), form.setFieldValue("avatar_url", value) - )} + onPickEmoji={(value) => form.setFieldValue("avatar_url", value)} /> From f3019e93fa1a1131d7a30b0ce2d6001584d2a24c Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 3 Jan 2024 19:59:05 +0000 Subject: [PATCH 3/6] add a suspense boundary --- site/src/components/IconField/IconField.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index 286ba89adb216..4479fdc86b48f 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -118,7 +118,9 @@ export const IconField: FC = ({ so we just have to sneak it into the DOM, which is kind of annoying, but means that users shouldn't ever spend time waiting for it to load. */}
- {}} /> + + {}} /> +
); From 8d2c0a5a38b9e312a6529b4b930bf5a4b15131a5 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 3 Jan 2024 21:48:45 +0000 Subject: [PATCH 4/6] :^) --- .../components/ErrorBoundary/ErrorBoundary.tsx | 15 ++++++++++----- site/src/components/IconField/IconField.tsx | 12 ++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/site/src/components/ErrorBoundary/ErrorBoundary.tsx b/site/src/components/ErrorBoundary/ErrorBoundary.tsx index 9267ae172b9b8..40b3a495c7d9f 100644 --- a/site/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/site/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,7 +1,10 @@ -import { Component, ReactNode, PropsWithChildren } from "react"; +import { Component, type ReactNode } from "react"; import { RuntimeErrorState } from "./RuntimeErrorState"; -type ErrorBoundaryProps = PropsWithChildren; +interface ErrorBoundaryProps { + fallback?: ReactNode; + children: ReactNode; +} interface ErrorBoundaryState { error: Error | null; @@ -9,7 +12,7 @@ interface ErrorBoundaryState { /** * Our app's Error Boundary - * Read more about React Error Boundaries: https://reactjs.org/docs/error-boundaries.html + * Read more about React Error Boundaries: https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary */ export class ErrorBoundary extends Component< ErrorBoundaryProps, @@ -20,13 +23,15 @@ export class ErrorBoundary extends Component< this.state = { error: null }; } - static getDerivedStateFromError(error: Error): { error: Error } { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { error }; } render(): ReactNode { if (this.state.error) { - return ; + return ( + this.props.fallback ?? + ); } return this.props.children; diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index 4479fdc86b48f..a39e4d9a4ab87 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -2,7 +2,9 @@ import { css, Global, useTheme } from "@emotion/react"; import Button from "@mui/material/Button"; import InputAdornment from "@mui/material/InputAdornment"; import TextField, { type TextFieldProps } from "@mui/material/TextField"; +import { visuallyHidden } from "@mui/utils"; import { type FC, lazy, Suspense } from "react"; +import { ErrorBoundary } from "components/ErrorBoundary/ErrorBoundary"; import { Loader } from "components/Loader/Loader"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack"; @@ -11,7 +13,6 @@ import { PopoverContent, PopoverTrigger, } from "components/Popover/Popover"; -import { visuallyHidden } from "@mui/utils"; // See: https://github.com/missive/emoji-mart/issues/51#issuecomment-287353222 const urlFromUnifiedCode = (unified: string) => @@ -118,9 +119,12 @@ export const IconField: FC = ({ so we just have to sneak it into the DOM, which is kind of annoying, but means that users shouldn't ever spend time waiting for it to load. */}
- - {}} /> - + {/* `ErrorBoundary` is for tests, this component requires `IntersectionObserver` */} + + + {}} /> + +
); From 746eac99e4d89253a89b5c65f5b1cd786ce76279 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 3 Jan 2024 22:26:53 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/src/components/IconField/IconField.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index a39e4d9a4ab87..5d01362c26c53 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -113,19 +113,21 @@ export const IconField: FC = ({ )} - {/* This component takes a long time to load (easily several seconds), so we + {/* + - This component takes a long time to load (easily several seconds), so we don't want to wait until the user actually clicks the button to start loading. Unfortunately, React doesn't provide an API to start warming a lazy component, so we just have to sneak it into the DOM, which is kind of annoying, but means - that users shouldn't ever spend time waiting for it to load. */} -
- {/* `ErrorBoundary` is for tests, this component requires `IntersectionObserver` */} - + that users shouldn't ever spend time waiting for it to load. + - Except we don't do it when running tests, because Jest doesn't define + `IntersectionObserver`, and it would make them slower anyway. */} + {process.env.NODE_ENV !== "test" && ( +
{}} /> - -
+
+ )} ); }; From 126c47ad97d96aba37c62dd71249b9e031044235 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 3 Jan 2024 22:32:24 +0000 Subject: [PATCH 6/6] ?:( --- site/src/components/IconField/IconField.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/components/IconField/IconField.tsx b/site/src/components/IconField/IconField.tsx index 5d01362c26c53..aaf9fa096cccb 100644 --- a/site/src/components/IconField/IconField.tsx +++ b/site/src/components/IconField/IconField.tsx @@ -4,7 +4,6 @@ import InputAdornment from "@mui/material/InputAdornment"; import TextField, { type TextFieldProps } from "@mui/material/TextField"; import { visuallyHidden } from "@mui/utils"; import { type FC, lazy, Suspense } from "react"; -import { ErrorBoundary } from "components/ErrorBoundary/ErrorBoundary"; import { Loader } from "components/Loader/Loader"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack";