From cc34db1659e2ae8ee0b86a57d291e8e1321d6941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Wed, 25 Oct 2023 17:14:18 +0100 Subject: [PATCH] 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 --- .../admin/banners/BannerModal/BannerForm.tsx | 367 +++++++++++------- .../admin/banners/BannerModal/BannerModal.tsx | 4 +- .../banners/BannersTable/BannersTable.tsx | 6 +- .../src/component/banners/Banner/Banner.tsx | 18 +- .../common/FormSwitch/FormSwitch.tsx | 10 +- 5 files changed, 256 insertions(+), 149 deletions(-) diff --git a/frontend/src/component/admin/banners/BannerModal/BannerForm.tsx b/frontend/src/component/admin/banners/BannerModal/BannerForm.tsx index 64762e7696b5..19535c6280b7 100644 --- a/frontend/src/component/admin/banners/BannerModal/BannerForm.tsx +++ b/frontend/src/component/admin/banners/BannerModal/BannerForm.tsx @@ -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( icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom', ); @@ -102,8 +140,10 @@ export const BannerForm = ({ return ( - - Preview: + + + Banner preview: + - - - - What is your banner message? - -

Markdown is supported.

- + + + + Banner status + + + + Configuration + + + What type of banner is it? + + + setVariant(variant as BannerVariant) } + options={VARIANT_OPTIONS} /> -
- ) => - setMessage(e.target.value) - } - autoComplete='off' - required - /> -
- - - What type of banner is it? - - setVariant(variant as BannerVariant)} - options={VARIANT_OPTIONS} - /> - - - - What icon should be displayed on the banner? - - { - setIconOption(iconOption as IconOption); - if (iconOption === 'None') { - setIcon('none'); - } else { - setIcon(''); - } - }} - options={['Default', 'Custom', 'None'].map((option) => ({ - key: option, - label: option, - }))} - /> + + + + What icon should be displayed on the banner? + + { + setIconOption(iconOption as IconOption); + if (iconOption === 'None') { + setIcon('none'); + } else { + setIcon(''); + } + }} + options={['Default', 'Custom', 'None'].map( + (option) => ({ + key: option, + label: option, + }), + )} + /> + + - What custom icon should be displayed? + Which custom icon?

@@ -212,34 +243,75 @@ export const BannerForm = ({ } autoComplete='off' /> - + } /> - - - - What action should be available in the banner? - - { - setLinkOption(linkOption as LinkOption); - if (linkOption === 'Dialog') { - setLink('dialog'); - } else { - setLink(''); + + + What is your banner message? + +

+ + Markdown + {' '} + is supported. +

+ + } + /> +
+ ) => + setMessage(e.target.value) } - }} - options={['None', 'Link', 'Dialog'].map((option) => ({ - key: option, - label: option, - }))} - /> + autoComplete='off' + required + /> +
+ + + Banner action + + + What action should be available in the banner? + + { + setLinkOption(linkOption as LinkOption); + if (linkOption === 'Dialog') { + setLink('dialog'); + } else { + setLink(''); + } + setLinkText(''); + setDialogTitle(''); + setDialog(''); + }} + options={['None', 'Link', 'Dialog'].map((option) => ({ + key: option, + label: option, + }))} + /> + + What URL should be opened? @@ -254,13 +326,13 @@ export const BannerForm = ({ }} autoComplete='off' /> - + } /> + What is the action text? @@ -272,72 +344,89 @@ export const BannerForm = ({ } autoComplete='off' /> - + } /> - - What is the dialog title? - - ) => - setDialogTitle(e.target.value) - } - autoComplete='off' - /> - - What is the dialog content? - -

Markdown is supported.

- - } + + + What is the dialog title? + + , + ) => setDialogTitle(e.target.value)} + autoComplete='off' /> -
- ) => - setDialog(e.target.value) - } - autoComplete='off' - /> + + + + What is the dialog content? + +

+ + Markdown + {' '} + is supported. +

+ + } + /> +
+ , + ) => setDialog(e.target.value)} + autoComplete='off' + /> +
+ } + onClick={() => setPreviewDialogOpen(true)} + > + Preview dialog + + + {dialog!} + } /> - - - - Is the banner sticky on the screen when scrolling? - - - - - - Is the banner currently visible to all users? - - + + Sticky banner + setSticky(e.target.checked)} + /> + } + label='Make the banner sticky on the screen when scrolling' /> - +
); }; diff --git a/frontend/src/component/admin/banners/BannerModal/BannerModal.tsx b/frontend/src/component/admin/banners/BannerModal/BannerModal.tsx index 0e030bb07e56..8eb1f6dc8278 100644 --- a/frontend/src/component/admin/banners/BannerModal/BannerModal.tsx +++ b/frontend/src/component/admin/banners/BannerModal/BannerModal.tsx @@ -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 = () => { diff --git a/frontend/src/component/admin/banners/BannersTable/BannersTable.tsx b/frontend/src/component/admin/banners/BannersTable/BannersTable.tsx index b021d587f94a..ecf6a6c63eb4 100644 --- a/frontend/src/component/admin/banners/BannersTable/BannersTable.tsx +++ b/frontend/src/component/admin/banners/BannersTable/BannersTable.tsx @@ -75,7 +75,11 @@ export const BannersTable = () => { Header: 'Banner', accessor: 'message', Cell: ({ row: { original: banner } }: any) => ( - + ), disableSortBy: true, minWidth: 200, diff --git a/frontend/src/component/banners/Banner/Banner.tsx b/frontend/src/component/banners/Banner/Banner.tsx index 96b3ad0a1dcd..652c6c0f746e 100644 --- a/frontend/src/component/banners/Banner/Banner.tsx +++ b/frontend/src/component/banners/Banner/Banner.tsx @@ -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 = ( - + diff --git a/frontend/src/component/common/FormSwitch/FormSwitch.tsx b/frontend/src/component/common/FormSwitch/FormSwitch.tsx index 02e2adc8ec25..f4f21b264553 100644 --- a/frontend/src/component/common/FormSwitch/FormSwitch.tsx +++ b/frontend/src/component/common/FormSwitch/FormSwitch.tsx @@ -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 ( {children} -