From b6712ffbee17a721426a0a5f5b0a34f8ce6310cd Mon Sep 17 00:00:00 2001 From: Presley Pizzo <1290996+presleyp@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:55:00 -0400 Subject: [PATCH] chore: add wrapper components for conditional rendering (#4047) * Add conditional wrappers * Use wrappers in TemplatesPageView --- .../Conditionals/ChooseOne.stories.tsx | 36 +++ .../src/components/Conditionals/ChooseOne.tsx | 29 +++ .../components/Conditionals/Maybe.stories.tsx | 19 ++ site/src/components/Conditionals/Maybe.tsx | 17 ++ .../pages/TemplatesPage/TemplatesPageView.tsx | 211 +++++++++--------- 5 files changed, 212 insertions(+), 100 deletions(-) create mode 100644 site/src/components/Conditionals/ChooseOne.stories.tsx create mode 100644 site/src/components/Conditionals/ChooseOne.tsx create mode 100644 site/src/components/Conditionals/Maybe.stories.tsx create mode 100644 site/src/components/Conditionals/Maybe.tsx diff --git a/site/src/components/Conditionals/ChooseOne.stories.tsx b/site/src/components/Conditionals/ChooseOne.stories.tsx new file mode 100644 index 0000000000..f2fc75fd43 --- /dev/null +++ b/site/src/components/Conditionals/ChooseOne.stories.tsx @@ -0,0 +1,36 @@ +import { Story } from "@storybook/react" +import { ChooseOne, Cond } from "./ChooseOne" + +export default { + title: "components/Conditionals/ChooseOne", + component: ChooseOne, + subcomponents: { Cond }, +} + +export const FirstIsTrue: Story = () => ( + + The first one shows. + The second one does not show. + +) + +export const SecondIsTrue: Story = () => ( + + The first one does not show. + The second one shows. + +) + +export const AllAreTrue: Story = () => ( + + Only the first one shows. + The second one does not show. + +) + +export const NoneAreTrue: Story = () => ( + + The first one does not show. + The second shows because it is the fallback. + +) diff --git a/site/src/components/Conditionals/ChooseOne.tsx b/site/src/components/Conditionals/ChooseOne.tsx new file mode 100644 index 0000000000..71b28d95db --- /dev/null +++ b/site/src/components/Conditionals/ChooseOne.tsx @@ -0,0 +1,29 @@ +import { Children, PropsWithChildren } from "react" + +export interface CondProps { + condition: boolean +} + +/** + * Wrapper component that attaches a condition to a child component so that ChooseOne can + * determine which child to render. The last Cond in a ChooseOne is the fallback case; set + * its `condition` to `true` to avoid confusion. + * @param condition boolean expression indicating whether the child should be rendered + * @returns child. Note that Cond alone does not enforce the condition; it should be used inside ChooseOne. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const Cond = ({ children, condition }: PropsWithChildren): JSX.Element => { + return <>{children} +} + +/** + * Wrapper component for rendering exactly one of its children. Wrap each child in Cond to associate it + * with a condition under which it should be rendered. If no conditions are met, the final child + * will be rendered. + * @returns one of its children + */ +export const ChooseOne = ({ children }: PropsWithChildren): JSX.Element => { + const childArray = Children.toArray(children) as JSX.Element[] + const chosen = childArray.find((child) => child.props.condition) + return chosen ?? childArray[childArray.length - 1] +} diff --git a/site/src/components/Conditionals/Maybe.stories.tsx b/site/src/components/Conditionals/Maybe.stories.tsx new file mode 100644 index 0000000000..d1baf45b13 --- /dev/null +++ b/site/src/components/Conditionals/Maybe.stories.tsx @@ -0,0 +1,19 @@ +import { Story } from "@storybook/react" +import { Maybe, MaybeProps } from "./Maybe" + +export default { + title: "components/Conditionals/Maybe", + component: Maybe, +} + +const Template: Story = (args: MaybeProps) => Now you see me + +export const ConditionIsTrue = Template.bind({}) +ConditionIsTrue.args = { + condition: true, +} + +export const ConditionIsFalse = Template.bind({}) +ConditionIsFalse.args = { + condition: false, +} diff --git a/site/src/components/Conditionals/Maybe.tsx b/site/src/components/Conditionals/Maybe.tsx new file mode 100644 index 0000000000..63af6ddb19 --- /dev/null +++ b/site/src/components/Conditionals/Maybe.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from "react" + +export interface MaybeProps { + condition: boolean +} + +/** + * Wrapper component for conditionally rendering a child component without using "curly brace mode." + * @param condition boolean expression that determines whether the child will be rendered + * @returns the child or null + */ +export const Maybe = ({ + children, + condition, +}: PropsWithChildren): JSX.Element | null => { + return condition ? <>{children} : null +} diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 76f5a89d50..64a3518a80 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -8,6 +8,8 @@ import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import useTheme from "@material-ui/styles/useTheme" +import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" +import { Maybe } from "components/Conditionals/Maybe" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { FC } from "react" import { useTranslation } from "react-i18next" @@ -103,7 +105,7 @@ export const TemplatesPageView: FC - {props.templates && props.templates.length > 0 && ( + 0)}> Choose a template to create a new workspace {props.canCreateTemplate ? ( @@ -121,113 +123,122 @@ export const TemplatesPageView: FC - )} + - {props.getOrganizationsError ? ( - - ) : props.getTemplatesError ? ( - - ) : ( - - - - - {Language.nameLabel} - {Language.usedByLabel} - {Language.lastUpdatedLabel} - {Language.createdByLabel} - - - - - {props.loading && } - - {empty ? ( + + + + + + + + + +
+ - - } - /> - + {Language.nameLabel} + {Language.usedByLabel} + {Language.lastUpdatedLabel} + {Language.createdByLabel} + - ) : ( - props.templates?.map((template) => { - const templatePageLink = `/templates/${template.name}` - const hasIcon = template.icon && template.icon !== "" + + + + + - return ( - { - if (event.key === "Enter") { - navigate(templatePageLink) - } - }} - className={styles.clickableTableRow} - > - - - - - ) + + + + + } /> - - - - - {Language.developerCount(template.active_user_count)} - - - - - - {createDayString(template.updated_at)} - - - - - - {template.created_by_name} - - - - -
- -
-
+
- ) - }) - )} -
-
-
- )} + + + {props.templates?.map((template) => { + const templatePageLink = `/templates/${template.name}` + const hasIcon = template.icon && template.icon !== "" + + return ( + { + if (event.key === "Enter") { + navigate(templatePageLink) + } + }} + className={styles.clickableTableRow} + > + + + + + ) + } + /> + + + + + {Language.developerCount(template.active_user_count)} + + + + + + {createDayString(template.updated_at)} + + + + + + {template.created_by_name} + + + + +
+ +
+
+
+ ) + })} +
+ + + + + + ) }