mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add wrapper components for conditional rendering (#4047)
* Add conditional wrappers * Use wrappers in TemplatesPageView
This commit is contained in:
@@ -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 = () => (
|
||||
<ChooseOne>
|
||||
<Cond condition>The first one shows.</Cond>
|
||||
<Cond condition={false}>The second one does not show.</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
|
||||
export const SecondIsTrue: Story = () => (
|
||||
<ChooseOne>
|
||||
<Cond condition={false}>The first one does not show.</Cond>
|
||||
<Cond condition>The second one shows.</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
|
||||
export const AllAreTrue: Story = () => (
|
||||
<ChooseOne>
|
||||
<Cond condition>Only the first one shows.</Cond>
|
||||
<Cond condition>The second one does not show.</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
|
||||
export const NoneAreTrue: Story = () => (
|
||||
<ChooseOne>
|
||||
<Cond condition={false}>The first one does not show.</Cond>
|
||||
<Cond condition={false}>The second shows because it is the fallback.</Cond>
|
||||
</ChooseOne>
|
||||
)
|
||||
@@ -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<CondProps>): 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]
|
||||
}
|
||||
@@ -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<MaybeProps> = (args: MaybeProps) => <Maybe {...args}>Now you see me</Maybe>
|
||||
|
||||
export const ConditionIsTrue = Template.bind({})
|
||||
ConditionIsTrue.args = {
|
||||
condition: true,
|
||||
}
|
||||
|
||||
export const ConditionIsFalse = Template.bind({})
|
||||
ConditionIsFalse.args = {
|
||||
condition: false,
|
||||
}
|
||||
@@ -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<MaybeProps>): JSX.Element | null => {
|
||||
return condition ? <>{children}</> : null
|
||||
}
|
||||
@@ -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<React.PropsWithChildren<TemplatesPageViewProp
|
||||
<TemplateHelpTooltip />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
{props.templates && props.templates.length > 0 && (
|
||||
<Maybe condition={Boolean(props.templates && props.templates.length > 0)}>
|
||||
<PageHeaderSubtitle>
|
||||
Choose a template to create a new workspace
|
||||
{props.canCreateTemplate ? (
|
||||
@@ -121,113 +123,122 @@ export const TemplatesPageView: FC<React.PropsWithChildren<TemplatesPageViewProp
|
||||
"."
|
||||
)}
|
||||
</PageHeaderSubtitle>
|
||||
)}
|
||||
</Maybe>
|
||||
</PageHeader>
|
||||
|
||||
{props.getOrganizationsError ? (
|
||||
<ErrorSummary
|
||||
error={props.getOrganizationsError}
|
||||
defaultMessage={t("errors.getOrganizationsError")}
|
||||
/>
|
||||
) : props.getTemplatesError ? (
|
||||
<ErrorSummary
|
||||
error={props.getTemplatesError}
|
||||
defaultMessage={t("errors.getTemplatesError")}
|
||||
/>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell width="50%">{Language.nameLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.usedByLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.createdByLabel}</TableCell>
|
||||
<TableCell width="1%"></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.loading && <TableLoader />}
|
||||
|
||||
{empty ? (
|
||||
<ChooseOne>
|
||||
<Cond condition={Boolean(props.getOrganizationsError)}>
|
||||
<ErrorSummary
|
||||
error={props.getOrganizationsError}
|
||||
defaultMessage={t("errors.getOrganizationsError")}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond condition={Boolean(props.getTemplatesError)}>
|
||||
<ErrorSummary
|
||||
error={props.getTemplatesError}
|
||||
defaultMessage={t("errors.getTemplatesError")}
|
||||
/>
|
||||
</Cond>
|
||||
<Cond condition>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={Language.emptyMessage}
|
||||
description={
|
||||
props.canCreateTemplate
|
||||
? Language.emptyDescription
|
||||
: Language.emptyViewNoPerms
|
||||
}
|
||||
descriptionClassName={styles.emptyDescription}
|
||||
cta={<CodeExample code="coder templates init" />}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell width="50%">{Language.nameLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.usedByLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.lastUpdatedLabel}</TableCell>
|
||||
<TableCell width="16%">{Language.createdByLabel}</TableCell>
|
||||
<TableCell width="1%"></TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
props.templates?.map((template) => {
|
||||
const templatePageLink = `/templates/${template.name}`
|
||||
const hasIcon = template.icon && template.icon !== ""
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<Maybe condition={Boolean(props.loading)}>
|
||||
<TableLoader />
|
||||
</Maybe>
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={template.id}
|
||||
hover
|
||||
data-testid={`template-${template.id}`}
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
navigate(templatePageLink)
|
||||
}
|
||||
}}
|
||||
className={styles.clickableTableRow}
|
||||
>
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<AvatarData
|
||||
title={template.name}
|
||||
subtitle={template.description}
|
||||
highlightTitle
|
||||
avatar={
|
||||
hasIcon && (
|
||||
<div className={styles.templateIconWrapper}>
|
||||
<img alt="" src={template.icon} />
|
||||
</div>
|
||||
)
|
||||
<ChooseOne>
|
||||
<Cond condition={empty}>
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={Language.emptyMessage}
|
||||
description={
|
||||
props.canCreateTemplate
|
||||
? Language.emptyDescription
|
||||
: Language.emptyViewNoPerms
|
||||
}
|
||||
descriptionClassName={styles.emptyDescription}
|
||||
cta={<CodeExample code="coder templates init" />}
|
||||
/>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{Language.developerCount(template.active_user_count)}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{createDayString(template.updated_at)}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{template.created_by_name}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<div className={styles.arrowCell}>
|
||||
<KeyboardArrowRight className={styles.arrowRight} />
|
||||
</div>
|
||||
</TableCellLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Cond>
|
||||
<Cond condition>
|
||||
{props.templates?.map((template) => {
|
||||
const templatePageLink = `/templates/${template.name}`
|
||||
const hasIcon = template.icon && template.icon !== ""
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={template.id}
|
||||
hover
|
||||
data-testid={`template-${template.id}`}
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
navigate(templatePageLink)
|
||||
}
|
||||
}}
|
||||
className={styles.clickableTableRow}
|
||||
>
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<AvatarData
|
||||
title={template.name}
|
||||
subtitle={template.description}
|
||||
highlightTitle
|
||||
avatar={
|
||||
hasIcon && (
|
||||
<div className={styles.templateIconWrapper}>
|
||||
<img alt="" src={template.icon} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{Language.developerCount(template.active_user_count)}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink data-chromatic="ignore" to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{createDayString(template.updated_at)}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<span style={{ color: theme.palette.text.secondary }}>
|
||||
{template.created_by_name}
|
||||
</span>
|
||||
</TableCellLink>
|
||||
|
||||
<TableCellLink to={templatePageLink}>
|
||||
<div className={styles.arrowCell}>
|
||||
<KeyboardArrowRight className={styles.arrowRight} />
|
||||
</div>
|
||||
</TableCellLink>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Cond>
|
||||
</ChooseOne>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user