1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

feat: banner UI/UX adjustments (#5151)

https://linear.app/unleash/issue/2-1549/ui-align-with-uiux

Includes UI/UX adjustments to the banners feature after aligning with
@nicolaesocaciu

There are a lot of changes, but here are a few:
 - Redesigned preview section
 - Redesigned banner status (enabled) section
 - Reordered form fields to better fit the flow
 - Reordered fields in the side-panel payload to reflect order in the UI
 - Made inputs full width
 - Adjusted multiline fields
 - Added a link to Markdown's basic syntax examples
 - Added a "preview dialog" button
 - Updated `HelpIcon` usage to use the `htmlTooltip`
- Improved `Banner` inline design, added a maxHeight prop for usage
inside a table
 - Improved `FormSwitch` design


![image](https://github.com/Unleash/unleash/assets/14320932/d8fe1f59-85ed-48eb-aa46-62628b12f0b1)

Co-authored-by: Nicolae <nicolae@getunleash.ai>
This commit is contained in:
Nuno Góis 2023-10-25 17:14:18 +01:00 committed by GitHub
parent 8e3863a27e
commit cc34db1659
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 256 additions and 149 deletions

View File

@ -1,4 +1,4 @@
import { styled } from '@mui/material'; import { Button, Checkbox, FormControlLabel, styled } from '@mui/material';
import { Banner } from 'component/banners/Banner/Banner'; import { Banner } from 'component/banners/Banner/Banner';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FormSwitch } from 'component/common/FormSwitch/FormSwitch'; import { FormSwitch } from 'component/common/FormSwitch/FormSwitch';
@ -7,6 +7,8 @@ import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import Input from 'component/common/Input/Input'; import Input from 'component/common/Input/Input';
import { BannerVariant } from 'interfaces/banner'; import { BannerVariant } from 'interfaces/banner';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { Visibility } from '@mui/icons-material';
import { BannerDialog } from 'component/banners/Banner/BannerDialog/BannerDialog';
const StyledForm = styled('form')(({ theme }) => ({ const StyledForm = styled('form')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -14,6 +16,37 @@ const StyledForm = styled('form')(({ theme }) => ({
gap: theme.spacing(4), gap: theme.spacing(4),
})); }));
const StyledBannerPreview = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: theme.spacing(1.5),
gap: theme.spacing(1.5),
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadiusMedium,
}));
const StyledBannerPreviewDescription = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));
const StyledRaisedSection = styled('div')(({ theme }) => ({
background: theme.palette.background.elevation1,
padding: theme.spacing(2, 3),
borderRadius: theme.shape.borderRadiusLarge,
}));
const StyledSection = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1.5),
}));
const StyledSectionLabel = styled('p')(({ theme }) => ({
fontWeight: theme.fontWeight.bold,
}));
const StyledFieldGroup = styled('div')(({ theme }) => ({ const StyledFieldGroup = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -25,10 +58,9 @@ const StyledInputDescription = styled('p')(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
})); }));
const StyledInput = styled(Input)(({ theme }) => ({ const StyledInput = styled(Input)({
width: '100%', width: '100%',
maxWidth: theme.spacing(50), });
}));
const StyledTooltip = styled('div')(({ theme }) => ({ const StyledTooltip = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -42,6 +74,10 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
maxWidth: theme.spacing(50), maxWidth: theme.spacing(50),
})); }));
const StyledPreviewButton = styled(Button)(({ theme }) => ({
marginRight: 'auto',
}));
const VARIANT_OPTIONS = [ const VARIANT_OPTIONS = [
{ key: 'info', label: 'Information' }, { key: 'info', label: 'Information' },
{ key: 'warning', label: 'Warning' }, { key: 'warning', label: 'Warning' },
@ -93,6 +129,8 @@ export const BannerForm = ({
setDialogTitle, setDialogTitle,
setDialog, setDialog,
}: IBannerFormProps) => { }: IBannerFormProps) => {
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [iconOption, setIconOption] = useState<IconOption>( const [iconOption, setIconOption] = useState<IconOption>(
icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom', icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom',
); );
@ -102,8 +140,10 @@ export const BannerForm = ({
return ( return (
<StyledForm> <StyledForm>
<StyledFieldGroup> <StyledBannerPreview>
<StyledInputDescription>Preview:</StyledInputDescription> <StyledBannerPreviewDescription>
Banner preview:
</StyledBannerPreviewDescription>
<Banner <Banner
banner={{ banner={{
message: message:
@ -119,67 +159,58 @@ export const BannerForm = ({
}} }}
inline inline
/> />
</StyledFieldGroup> </StyledBannerPreview>
<StyledFieldGroup> <StyledRaisedSection>
<StyledInputDescription> <FormSwitch checked={enabled} setChecked={setEnabled}>
What is your banner message? Banner status
<HelpIcon </FormSwitch>
tooltip={ </StyledRaisedSection>
<StyledTooltip> <StyledSection>
<p>Markdown is supported.</p> <StyledSectionLabel>Configuration</StyledSectionLabel>
</StyledTooltip> <StyledFieldGroup>
<StyledInputDescription>
What type of banner is it?
</StyledInputDescription>
<StyledSelect
size='small'
value={variant}
onChange={(variant) =>
setVariant(variant as BannerVariant)
} }
options={VARIANT_OPTIONS}
/> />
</StyledInputDescription> </StyledFieldGroup>
<StyledInput <StyledFieldGroup>
autoFocus <StyledInputDescription>
label='Banner message' What icon should be displayed on the banner?
value={message} </StyledInputDescription>
onChange={(e: ChangeEvent<HTMLInputElement>) => <StyledSelect
setMessage(e.target.value) size='small'
} value={iconOption}
autoComplete='off' onChange={(iconOption) => {
required setIconOption(iconOption as IconOption);
/> if (iconOption === 'None') {
</StyledFieldGroup> setIcon('none');
<StyledFieldGroup> } else {
<StyledInputDescription> setIcon('');
What type of banner is it? }
</StyledInputDescription> }}
<StyledSelect options={['Default', 'Custom', 'None'].map(
size='small' (option) => ({
value={variant} key: option,
onChange={(variant) => setVariant(variant as BannerVariant)} label: option,
options={VARIANT_OPTIONS} }),
/> )}
</StyledFieldGroup> />
<StyledFieldGroup> </StyledFieldGroup>
<StyledInputDescription>
What icon should be displayed on the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={iconOption}
onChange={(iconOption) => {
setIconOption(iconOption as IconOption);
if (iconOption === 'None') {
setIcon('none');
} else {
setIcon('');
}
}}
options={['Default', 'Custom', 'None'].map((option) => ({
key: option,
label: option,
}))}
/>
<ConditionallyRender <ConditionallyRender
condition={iconOption === 'Custom'} condition={iconOption === 'Custom'}
show={ show={
<> <StyledFieldGroup>
<StyledInputDescription> <StyledInputDescription>
What custom icon should be displayed? Which custom icon?
<HelpIcon <HelpIcon
htmlTooltip
tooltip={ tooltip={
<StyledTooltip> <StyledTooltip>
<p> <p>
@ -212,34 +243,75 @@ export const BannerForm = ({
} }
autoComplete='off' autoComplete='off'
/> />
</> </StyledFieldGroup>
} }
/> />
</StyledFieldGroup> <StyledFieldGroup>
<StyledFieldGroup> <StyledInputDescription>
<StyledInputDescription> What is your banner message?
What action should be available in the banner? <HelpIcon
</StyledInputDescription> htmlTooltip
<StyledSelect tooltip={
size='small' <StyledTooltip>
value={linkOption} <p>
onChange={(linkOption) => { <a
setLinkOption(linkOption as LinkOption); href='https://www.markdownguide.org/basic-syntax/'
if (linkOption === 'Dialog') { target='_blank'
setLink('dialog'); rel='noreferrer'
} else { >
setLink(''); Markdown
</a>{' '}
is supported.
</p>
</StyledTooltip>
}
/>
</StyledInputDescription>
<StyledInput
autoFocus
label='Banner message'
multiline
minRows={2}
maxRows={6}
value={message}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setMessage(e.target.value)
} }
}} autoComplete='off'
options={['None', 'Link', 'Dialog'].map((option) => ({ required
key: option, />
label: option, </StyledFieldGroup>
}))} </StyledSection>
/> <StyledSection>
<StyledSectionLabel>Banner action</StyledSectionLabel>
<StyledFieldGroup>
<StyledInputDescription>
What action should be available in the banner?
</StyledInputDescription>
<StyledSelect
size='small'
value={linkOption}
onChange={(linkOption) => {
setLinkOption(linkOption as LinkOption);
if (linkOption === 'Dialog') {
setLink('dialog');
} else {
setLink('');
}
setLinkText('');
setDialogTitle('');
setDialog('');
}}
options={['None', 'Link', 'Dialog'].map((option) => ({
key: option,
label: option,
}))}
/>
</StyledFieldGroup>
<ConditionallyRender <ConditionallyRender
condition={linkOption === 'Link'} condition={linkOption === 'Link'}
show={ show={
<> <StyledFieldGroup>
<StyledInputDescription> <StyledInputDescription>
What URL should be opened? What URL should be opened?
</StyledInputDescription> </StyledInputDescription>
@ -254,13 +326,13 @@ export const BannerForm = ({
}} }}
autoComplete='off' autoComplete='off'
/> />
</> </StyledFieldGroup>
} }
/> />
<ConditionallyRender <ConditionallyRender
condition={linkOption !== 'None'} condition={linkOption !== 'None'}
show={ show={
<> <StyledFieldGroup>
<StyledInputDescription> <StyledInputDescription>
What is the action text? What is the action text?
</StyledInputDescription> </StyledInputDescription>
@ -272,72 +344,89 @@ export const BannerForm = ({
} }
autoComplete='off' autoComplete='off'
/> />
</> </StyledFieldGroup>
} }
/> />
<ConditionallyRender <ConditionallyRender
condition={linkOption === 'Dialog'} condition={linkOption === 'Dialog'}
show={ show={
<> <>
<StyledInputDescription> <StyledFieldGroup>
What is the dialog title? <StyledInputDescription>
</StyledInputDescription> What is the dialog title?
<StyledInput </StyledInputDescription>
label='Dialog title' <StyledInput
value={dialogTitle} label='Dialog title'
onChange={(e: ChangeEvent<HTMLInputElement>) => value={dialogTitle}
setDialogTitle(e.target.value) onChange={(
} e: ChangeEvent<HTMLInputElement>,
autoComplete='off' ) => setDialogTitle(e.target.value)}
/> autoComplete='off'
<StyledInputDescription>
What is the dialog content?
<HelpIcon
tooltip={
<StyledTooltip>
<p>Markdown is supported.</p>
</StyledTooltip>
}
/> />
</StyledInputDescription> </StyledFieldGroup>
<StyledInput <StyledFieldGroup>
label='Dialog content' <StyledInputDescription>
multiline What is the dialog content?
minRows={4} <HelpIcon
value={dialog} htmlTooltip
onChange={(e: ChangeEvent<HTMLInputElement>) => tooltip={
setDialog(e.target.value) <StyledTooltip>
} <p>
autoComplete='off' <a
/> href='https://www.markdownguide.org/basic-syntax/'
target='_blank'
rel='noreferrer'
>
Markdown
</a>{' '}
is supported.
</p>
</StyledTooltip>
}
/>
</StyledInputDescription>
<StyledInput
label='Dialog content'
multiline
minRows={4}
value={dialog}
onChange={(
e: ChangeEvent<HTMLInputElement>,
) => setDialog(e.target.value)}
autoComplete='off'
/>
</StyledFieldGroup>
<StyledPreviewButton
variant='outlined'
color='primary'
startIcon={<Visibility />}
onClick={() => setPreviewDialogOpen(true)}
>
Preview dialog
</StyledPreviewButton>
<BannerDialog
open={previewDialogOpen}
setOpen={setPreviewDialogOpen}
title={dialogTitle || linkText}
>
{dialog!}
</BannerDialog>
</> </>
} }
/> />
</StyledFieldGroup> </StyledSection>
<StyledFieldGroup> <StyledSection>
<StyledInputDescription> <StyledSectionLabel>Sticky banner</StyledSectionLabel>
Is the banner sticky on the screen when scrolling? <FormControlLabel
</StyledInputDescription> control={
<FormSwitch <Checkbox
checked={sticky} checked={sticky}
setChecked={setSticky} onChange={(e) => setSticky(e.target.checked)}
sx={{ />
justifyContent: 'start', }
}} label='Make the banner sticky on the screen when scrolling'
/> />
</StyledFieldGroup> </StyledSection>
<StyledFieldGroup>
<StyledInputDescription>
Is the banner currently visible to all users?
</StyledInputDescription>
<FormSwitch
checked={enabled}
setChecked={setEnabled}
sx={{
justifyContent: 'start',
}}
/>
</StyledFieldGroup>
</StyledForm> </StyledForm>
); );
}; };

View File

@ -69,15 +69,15 @@ export const BannerModal = ({ banner, open, setOpen }: IBannerModalProps) => {
const isValid = message.length; const isValid = message.length;
const payload: AddOrUpdateBanner = { const payload: AddOrUpdateBanner = {
message, enabled,
variant, variant,
icon, icon,
message,
link, link,
linkText, linkText,
dialogTitle, dialogTitle,
dialog, dialog,
sticky, sticky,
enabled,
}; };
const formatApiCode = () => { const formatApiCode = () => {

View File

@ -75,7 +75,11 @@ export const BannersTable = () => {
Header: 'Banner', Header: 'Banner',
accessor: 'message', accessor: 'message',
Cell: ({ row: { original: banner } }: any) => ( Cell: ({ row: { original: banner } }: any) => (
<Banner banner={{ ...banner, sticky: false }} inline /> <Banner
banner={{ ...banner, sticky: false }}
inline
maxHeight={42}
/>
), ),
disableSortBy: true, disableSortBy: true,
minWidth: 200, minWidth: 200,

View File

@ -14,21 +14,28 @@ import { BannerVariant, IBanner } from 'interfaces/banner';
import { Sticky } from 'component/common/Sticky/Sticky'; import { Sticky } from 'component/common/Sticky/Sticky';
const StyledBar = styled('aside', { const StyledBar = styled('aside', {
shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'inline', shouldForwardProp: (prop) =>
})<{ variant: BannerVariant; inline?: boolean }>( prop !== 'variant' && prop !== 'inline' && prop !== 'maxHeight',
({ theme, variant, inline }) => ({ })<{ variant: BannerVariant; inline?: boolean; maxHeight?: number }>(
({ theme, variant, inline, maxHeight }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
padding: theme.spacing(1), padding: theme.spacing(1),
gap: theme.spacing(1), gap: theme.spacing(1),
width: '100%',
...(inline ...(inline
? { ? {
border: '1px solid', border: '1px solid',
borderRadius: theme.shape.borderRadiusMedium,
} }
: { : {
borderBottom: '1px solid', borderBottom: '1px solid',
}), }),
...(maxHeight && {
maxHeight: maxHeight,
overflow: 'auto',
}),
borderColor: theme.palette[variant].border, borderColor: theme.palette[variant].border,
background: theme.palette[variant].light, background: theme.palette[variant].light,
color: theme.palette[variant].dark, color: theme.palette[variant].dark,
@ -47,9 +54,10 @@ const StyledIcon = styled('div', {
interface IBannerProps { interface IBannerProps {
banner: IBanner; banner: IBanner;
inline?: boolean; inline?: boolean;
maxHeight?: number;
} }
export const Banner = ({ banner, inline }: IBannerProps) => { export const Banner = ({ banner, inline, maxHeight }: IBannerProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { const {
@ -65,7 +73,7 @@ export const Banner = ({ banner, inline }: IBannerProps) => {
} = banner; } = banner;
const bannerBar = ( const bannerBar = (
<StyledBar variant={variant} inline={inline}> <StyledBar variant={variant} inline={inline} maxHeight={maxHeight}>
<StyledIcon variant={variant}> <StyledIcon variant={variant}>
<BannerIcon icon={icon} variant={variant} /> <BannerIcon icon={icon} variant={variant} />
</StyledIcon> </StyledIcon>

View File

@ -1,15 +1,21 @@
import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material'; import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material';
import { Dispatch, ReactNode, SetStateAction } from 'react'; import { Dispatch, ReactNode, SetStateAction } from 'react';
const StyledContainer = styled(Box)({ const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
width: '100%', width: '100%',
lineHeight: theme.spacing(2.75),
}));
const StyledControlLabel = styled(FormControlLabel)({
marginRight: 0,
}); });
const StyledSwitchSpan = styled('span')(({ theme }) => ({ const StyledSwitchSpan = styled('span')(({ theme }) => ({
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),
fontSize: theme.fontSizes.smallBody,
})); }));
interface IFormSwitchProps extends BoxProps { interface IFormSwitchProps extends BoxProps {
@ -27,7 +33,7 @@ export const FormSwitch = ({
return ( return (
<StyledContainer {...props}> <StyledContainer {...props}>
{children} {children}
<FormControlLabel <StyledControlLabel
control={ control={
<Switch <Switch
checked={checked} checked={checked}