mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +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:
parent
8e3863a27e
commit
cc34db1659
@ -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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
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 { BannerVariant } from 'interfaces/banner';
|
||||
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 }) => ({
|
||||
display: 'flex',
|
||||
@ -14,6 +16,37 @@ const StyledForm = styled('form')(({ theme }) => ({
|
||||
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 }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@ -25,10 +58,9 @@ const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(({ theme }) => ({
|
||||
const StyledInput = styled(Input)({
|
||||
width: '100%',
|
||||
maxWidth: theme.spacing(50),
|
||||
}));
|
||||
});
|
||||
|
||||
const StyledTooltip = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -42,6 +74,10 @@ const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||
maxWidth: theme.spacing(50),
|
||||
}));
|
||||
|
||||
const StyledPreviewButton = styled(Button)(({ theme }) => ({
|
||||
marginRight: 'auto',
|
||||
}));
|
||||
|
||||
const VARIANT_OPTIONS = [
|
||||
{ key: 'info', label: 'Information' },
|
||||
{ key: 'warning', label: 'Warning' },
|
||||
@ -93,6 +129,8 @@ export const BannerForm = ({
|
||||
setDialogTitle,
|
||||
setDialog,
|
||||
}: IBannerFormProps) => {
|
||||
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
|
||||
|
||||
const [iconOption, setIconOption] = useState<IconOption>(
|
||||
icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom',
|
||||
);
|
||||
@ -102,8 +140,10 @@ export const BannerForm = ({
|
||||
|
||||
return (
|
||||
<StyledForm>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>Preview:</StyledInputDescription>
|
||||
<StyledBannerPreview>
|
||||
<StyledBannerPreviewDescription>
|
||||
Banner preview:
|
||||
</StyledBannerPreviewDescription>
|
||||
<Banner
|
||||
banner={{
|
||||
message:
|
||||
@ -119,67 +159,58 @@ export const BannerForm = ({
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What is your banner message?
|
||||
<HelpIcon
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>Markdown is supported.</p>
|
||||
</StyledTooltip>
|
||||
</StyledBannerPreview>
|
||||
<StyledRaisedSection>
|
||||
<FormSwitch checked={enabled} setChecked={setEnabled}>
|
||||
Banner status
|
||||
</FormSwitch>
|
||||
</StyledRaisedSection>
|
||||
<StyledSection>
|
||||
<StyledSectionLabel>Configuration</StyledSectionLabel>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What type of banner is it?
|
||||
</StyledInputDescription>
|
||||
<StyledSelect
|
||||
size='small'
|
||||
value={variant}
|
||||
onChange={(variant) =>
|
||||
setVariant(variant as BannerVariant)
|
||||
}
|
||||
options={VARIANT_OPTIONS}
|
||||
/>
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
label='Banner message'
|
||||
value={message}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setMessage(e.target.value)
|
||||
}
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What type of banner is it?
|
||||
</StyledInputDescription>
|
||||
<StyledSelect
|
||||
size='small'
|
||||
value={variant}
|
||||
onChange={(variant) => setVariant(variant as BannerVariant)}
|
||||
options={VARIANT_OPTIONS}
|
||||
/>
|
||||
</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,
|
||||
}))}
|
||||
/>
|
||||
</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,
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<ConditionallyRender
|
||||
condition={iconOption === 'Custom'}
|
||||
show={
|
||||
<>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What custom icon should be displayed?
|
||||
Which custom icon?
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>
|
||||
@ -212,34 +243,75 @@ export const BannerForm = ({
|
||||
}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</>
|
||||
</StyledFieldGroup>
|
||||
}
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<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('');
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What is your banner message?
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>
|
||||
<a
|
||||
href='https://www.markdownguide.org/basic-syntax/'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
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)
|
||||
}
|
||||
}}
|
||||
options={['None', 'Link', 'Dialog'].map((option) => ({
|
||||
key: option,
|
||||
label: option,
|
||||
}))}
|
||||
/>
|
||||
autoComplete='off'
|
||||
required
|
||||
/>
|
||||
</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
|
||||
condition={linkOption === 'Link'}
|
||||
show={
|
||||
<>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What URL should be opened?
|
||||
</StyledInputDescription>
|
||||
@ -254,13 +326,13 @@ export const BannerForm = ({
|
||||
}}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</>
|
||||
</StyledFieldGroup>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={linkOption !== 'None'}
|
||||
show={
|
||||
<>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What is the action text?
|
||||
</StyledInputDescription>
|
||||
@ -272,72 +344,89 @@ export const BannerForm = ({
|
||||
}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</>
|
||||
</StyledFieldGroup>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={linkOption === 'Dialog'}
|
||||
show={
|
||||
<>
|
||||
<StyledInputDescription>
|
||||
What is the dialog title?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
label='Dialog title'
|
||||
value={dialogTitle}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setDialogTitle(e.target.value)
|
||||
}
|
||||
autoComplete='off'
|
||||
/>
|
||||
<StyledInputDescription>
|
||||
What is the dialog content?
|
||||
<HelpIcon
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>Markdown is supported.</p>
|
||||
</StyledTooltip>
|
||||
}
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What is the dialog title?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
label='Dialog title'
|
||||
value={dialogTitle}
|
||||
onChange={(
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
) => setDialogTitle(e.target.value)}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
label='Dialog content'
|
||||
multiline
|
||||
minRows={4}
|
||||
value={dialog}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setDialog(e.target.value)
|
||||
}
|
||||
autoComplete='off'
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
What is the dialog content?
|
||||
<HelpIcon
|
||||
htmlTooltip
|
||||
tooltip={
|
||||
<StyledTooltip>
|
||||
<p>
|
||||
<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>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
Is the banner sticky on the screen when scrolling?
|
||||
</StyledInputDescription>
|
||||
<FormSwitch
|
||||
checked={sticky}
|
||||
setChecked={setSticky}
|
||||
sx={{
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
</StyledSection>
|
||||
<StyledSection>
|
||||
<StyledSectionLabel>Sticky banner</StyledSectionLabel>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={sticky}
|
||||
onChange={(e) => setSticky(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label='Make the banner sticky on the screen when scrolling'
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
<StyledFieldGroup>
|
||||
<StyledInputDescription>
|
||||
Is the banner currently visible to all users?
|
||||
</StyledInputDescription>
|
||||
<FormSwitch
|
||||
checked={enabled}
|
||||
setChecked={setEnabled}
|
||||
sx={{
|
||||
justifyContent: 'start',
|
||||
}}
|
||||
/>
|
||||
</StyledFieldGroup>
|
||||
</StyledSection>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
||||
|
@ -69,15 +69,15 @@ export const BannerModal = ({ banner, open, setOpen }: IBannerModalProps) => {
|
||||
const isValid = message.length;
|
||||
|
||||
const payload: AddOrUpdateBanner = {
|
||||
message,
|
||||
enabled,
|
||||
variant,
|
||||
icon,
|
||||
message,
|
||||
link,
|
||||
linkText,
|
||||
dialogTitle,
|
||||
dialog,
|
||||
sticky,
|
||||
enabled,
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
|
@ -75,7 +75,11 @@ export const BannersTable = () => {
|
||||
Header: 'Banner',
|
||||
accessor: 'message',
|
||||
Cell: ({ row: { original: banner } }: any) => (
|
||||
<Banner banner={{ ...banner, sticky: false }} inline />
|
||||
<Banner
|
||||
banner={{ ...banner, sticky: false }}
|
||||
inline
|
||||
maxHeight={42}
|
||||
/>
|
||||
),
|
||||
disableSortBy: true,
|
||||
minWidth: 200,
|
||||
|
@ -14,21 +14,28 @@ import { BannerVariant, IBanner } from 'interfaces/banner';
|
||||
import { Sticky } from 'component/common/Sticky/Sticky';
|
||||
|
||||
const StyledBar = styled('aside', {
|
||||
shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'inline',
|
||||
})<{ variant: BannerVariant; inline?: boolean }>(
|
||||
({ theme, variant, inline }) => ({
|
||||
shouldForwardProp: (prop) =>
|
||||
prop !== 'variant' && prop !== 'inline' && prop !== 'maxHeight',
|
||||
})<{ variant: BannerVariant; inline?: boolean; maxHeight?: number }>(
|
||||
({ theme, variant, inline, maxHeight }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
width: '100%',
|
||||
...(inline
|
||||
? {
|
||||
border: '1px solid',
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
}
|
||||
: {
|
||||
borderBottom: '1px solid',
|
||||
}),
|
||||
...(maxHeight && {
|
||||
maxHeight: maxHeight,
|
||||
overflow: 'auto',
|
||||
}),
|
||||
borderColor: theme.palette[variant].border,
|
||||
background: theme.palette[variant].light,
|
||||
color: theme.palette[variant].dark,
|
||||
@ -47,9 +54,10 @@ const StyledIcon = styled('div', {
|
||||
interface IBannerProps {
|
||||
banner: IBanner;
|
||||
inline?: boolean;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const Banner = ({ banner, inline }: IBannerProps) => {
|
||||
export const Banner = ({ banner, inline, maxHeight }: IBannerProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {
|
||||
@ -65,7 +73,7 @@ export const Banner = ({ banner, inline }: IBannerProps) => {
|
||||
} = banner;
|
||||
|
||||
const bannerBar = (
|
||||
<StyledBar variant={variant} inline={inline}>
|
||||
<StyledBar variant={variant} inline={inline} maxHeight={maxHeight}>
|
||||
<StyledIcon variant={variant}>
|
||||
<BannerIcon icon={icon} variant={variant} />
|
||||
</StyledIcon>
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material';
|
||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
|
||||
const StyledContainer = styled(Box)({
|
||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
lineHeight: theme.spacing(2.75),
|
||||
}));
|
||||
|
||||
const StyledControlLabel = styled(FormControlLabel)({
|
||||
marginRight: 0,
|
||||
});
|
||||
|
||||
const StyledSwitchSpan = styled('span')(({ theme }) => ({
|
||||
marginLeft: theme.spacing(0.5),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
interface IFormSwitchProps extends BoxProps {
|
||||
@ -27,7 +33,7 @@ export const FormSwitch = ({
|
||||
return (
|
||||
<StyledContainer {...props}>
|
||||
{children}
|
||||
<FormControlLabel
|
||||
<StyledControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={checked}
|
||||
|
Loading…
Reference in New Issue
Block a user