mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: banner modal (#5132)
https://linear.app/unleash/issue/2-1548/ui-create-banner-newedit-modal Adds a new banner modal (and form) that allows admins to create and edit banners. Also adds a new `FormSwitch` common component that may be helpful in different situations where we need a switch on a form. <img width="1263" alt="image" src="https://github.com/Unleash/unleash/assets/14320932/1b89db9b-9003-413c-8829-c37d245e2487">
This commit is contained in:
		
							parent
							
								
									898c1b4bc7
								
							
						
					
					
						commit
						3ca22c7c5c
					
				
							
								
								
									
										343
									
								
								frontend/src/component/admin/banners/BannerModal/BannerForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								frontend/src/component/admin/banners/BannerModal/BannerForm.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,343 @@
 | 
			
		||||
import { 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';
 | 
			
		||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(4),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledFieldGroup = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledInputDescription = styled('p')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    color: theme.palette.text.primary,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledInput = styled(Input)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    maxWidth: theme.spacing(50),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTooltip = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    padding: theme.spacing(0.5),
 | 
			
		||||
    gap: theme.spacing(0.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    maxWidth: theme.spacing(50),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const VARIANT_OPTIONS = [
 | 
			
		||||
    { key: 'info', label: 'Information' },
 | 
			
		||||
    { key: 'warning', label: 'Warning' },
 | 
			
		||||
    { key: 'error', label: 'Error' },
 | 
			
		||||
    { key: 'success', label: 'Success' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
type IconOption = 'Default' | 'Custom' | 'None';
 | 
			
		||||
type LinkOption = 'Link' | 'Dialog' | 'None';
 | 
			
		||||
 | 
			
		||||
interface IBannerFormProps {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    message: string;
 | 
			
		||||
    variant: BannerVariant;
 | 
			
		||||
    sticky: boolean;
 | 
			
		||||
    icon: string;
 | 
			
		||||
    link: string;
 | 
			
		||||
    linkText: string;
 | 
			
		||||
    dialogTitle: string;
 | 
			
		||||
    dialog: string;
 | 
			
		||||
    setEnabled: Dispatch<SetStateAction<boolean>>;
 | 
			
		||||
    setMessage: Dispatch<SetStateAction<string>>;
 | 
			
		||||
    setVariant: Dispatch<SetStateAction<BannerVariant>>;
 | 
			
		||||
    setSticky: Dispatch<SetStateAction<boolean>>;
 | 
			
		||||
    setIcon: Dispatch<SetStateAction<string>>;
 | 
			
		||||
    setLink: Dispatch<SetStateAction<string>>;
 | 
			
		||||
    setLinkText: Dispatch<SetStateAction<string>>;
 | 
			
		||||
    setDialogTitle: Dispatch<SetStateAction<string>>;
 | 
			
		||||
    setDialog: Dispatch<SetStateAction<string>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const BannerForm = ({
 | 
			
		||||
    enabled,
 | 
			
		||||
    message,
 | 
			
		||||
    variant,
 | 
			
		||||
    sticky,
 | 
			
		||||
    icon,
 | 
			
		||||
    link,
 | 
			
		||||
    linkText,
 | 
			
		||||
    dialogTitle,
 | 
			
		||||
    dialog,
 | 
			
		||||
    setEnabled,
 | 
			
		||||
    setMessage,
 | 
			
		||||
    setVariant,
 | 
			
		||||
    setSticky,
 | 
			
		||||
    setIcon,
 | 
			
		||||
    setLink,
 | 
			
		||||
    setLinkText,
 | 
			
		||||
    setDialogTitle,
 | 
			
		||||
    setDialog,
 | 
			
		||||
}: IBannerFormProps) => {
 | 
			
		||||
    const [iconOption, setIconOption] = useState<IconOption>(
 | 
			
		||||
        icon === '' ? 'Default' : icon === 'none' ? 'None' : 'Custom',
 | 
			
		||||
    );
 | 
			
		||||
    const [linkOption, setLinkOption] = useState<LinkOption>(
 | 
			
		||||
        link === '' ? 'None' : link === 'dialog' ? 'Dialog' : 'Link',
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledForm>
 | 
			
		||||
            <StyledFieldGroup>
 | 
			
		||||
                <StyledInputDescription>Preview:</StyledInputDescription>
 | 
			
		||||
                <Banner
 | 
			
		||||
                    banner={{
 | 
			
		||||
                        message:
 | 
			
		||||
                            message ||
 | 
			
		||||
                            '*No message set. Please enter a message below.*',
 | 
			
		||||
                        variant,
 | 
			
		||||
                        sticky: false,
 | 
			
		||||
                        icon,
 | 
			
		||||
                        link,
 | 
			
		||||
                        linkText,
 | 
			
		||||
                        dialogTitle,
 | 
			
		||||
                        dialog,
 | 
			
		||||
                    }}
 | 
			
		||||
                    inline
 | 
			
		||||
                />
 | 
			
		||||
            </StyledFieldGroup>
 | 
			
		||||
            <StyledFieldGroup>
 | 
			
		||||
                <StyledInputDescription>
 | 
			
		||||
                    What is your banner message?
 | 
			
		||||
                    <HelpIcon
 | 
			
		||||
                        tooltip={
 | 
			
		||||
                            <StyledTooltip>
 | 
			
		||||
                                <p>Markdown is supported.</p>
 | 
			
		||||
                            </StyledTooltip>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </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,
 | 
			
		||||
                    }))}
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={iconOption === 'Custom'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <StyledInputDescription>
 | 
			
		||||
                                What custom icon should be displayed?
 | 
			
		||||
                                <HelpIcon
 | 
			
		||||
                                    tooltip={
 | 
			
		||||
                                        <StyledTooltip>
 | 
			
		||||
                                            <p>
 | 
			
		||||
                                                Choose an icon from{' '}
 | 
			
		||||
                                                <a
 | 
			
		||||
                                                    href='https://fonts.google.com/icons'
 | 
			
		||||
                                                    target='_blank'
 | 
			
		||||
                                                    rel='noreferrer'
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    Material Symbols
 | 
			
		||||
                                                </a>
 | 
			
		||||
                                                .
 | 
			
		||||
                                            </p>
 | 
			
		||||
                                            <p>
 | 
			
		||||
                                                For example, if you want to
 | 
			
		||||
                                                display the "Rocket Launch"
 | 
			
		||||
                                                icon, you can enter
 | 
			
		||||
                                                "rocket_launch" in the field
 | 
			
		||||
                                                below.
 | 
			
		||||
                                            </p>
 | 
			
		||||
                                        </StyledTooltip>
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </StyledInputDescription>
 | 
			
		||||
                            <StyledInput
 | 
			
		||||
                                label='Banner icon'
 | 
			
		||||
                                value={icon}
 | 
			
		||||
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
 | 
			
		||||
                                    setIcon(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                autoComplete='off'
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </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('');
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                    options={['None', 'Link', 'Dialog'].map((option) => ({
 | 
			
		||||
                        key: option,
 | 
			
		||||
                        label: option,
 | 
			
		||||
                    }))}
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={linkOption === 'Link'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <StyledInputDescription>
 | 
			
		||||
                                What URL should be opened?
 | 
			
		||||
                            </StyledInputDescription>
 | 
			
		||||
                            <StyledInput
 | 
			
		||||
                                label='URL'
 | 
			
		||||
                                value={link}
 | 
			
		||||
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
 | 
			
		||||
                                    setLink(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                onBlur={() => {
 | 
			
		||||
                                    if (!linkText) setLinkText(link);
 | 
			
		||||
                                }}
 | 
			
		||||
                                autoComplete='off'
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={linkOption !== 'None'}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <StyledInputDescription>
 | 
			
		||||
                                What is the action text?
 | 
			
		||||
                            </StyledInputDescription>
 | 
			
		||||
                            <StyledInput
 | 
			
		||||
                                label='Action text'
 | 
			
		||||
                                value={linkText}
 | 
			
		||||
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
 | 
			
		||||
                                    setLinkText(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                autoComplete='off'
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
                <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>
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </StyledInputDescription>
 | 
			
		||||
                            <StyledInput
 | 
			
		||||
                                label='Dialog content'
 | 
			
		||||
                                multiline
 | 
			
		||||
                                minRows={4}
 | 
			
		||||
                                value={dialog}
 | 
			
		||||
                                onChange={(e: ChangeEvent<HTMLInputElement>) =>
 | 
			
		||||
                                    setDialog(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                autoComplete='off'
 | 
			
		||||
                            />
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </StyledFieldGroup>
 | 
			
		||||
            <StyledFieldGroup>
 | 
			
		||||
                <StyledInputDescription>
 | 
			
		||||
                    Is the banner sticky on the screen when scrolling?
 | 
			
		||||
                </StyledInputDescription>
 | 
			
		||||
                <FormSwitch
 | 
			
		||||
                    checked={sticky}
 | 
			
		||||
                    setChecked={setSticky}
 | 
			
		||||
                    sx={{
 | 
			
		||||
                        justifyContent: 'start',
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
            </StyledFieldGroup>
 | 
			
		||||
            <StyledFieldGroup>
 | 
			
		||||
                <StyledInputDescription>
 | 
			
		||||
                    Is the banner currently visible to all users?
 | 
			
		||||
                </StyledInputDescription>
 | 
			
		||||
                <FormSwitch
 | 
			
		||||
                    checked={enabled}
 | 
			
		||||
                    setChecked={setEnabled}
 | 
			
		||||
                    sx={{
 | 
			
		||||
                        justifyContent: 'start',
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
            </StyledFieldGroup>
 | 
			
		||||
        </StyledForm>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										173
									
								
								frontend/src/component/admin/banners/BannerModal/BannerModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								frontend/src/component/admin/banners/BannerModal/BannerModal.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,173 @@
 | 
			
		||||
import { Button, styled } from '@mui/material';
 | 
			
		||||
import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
 | 
			
		||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { FormEvent, useEffect, useState } from 'react';
 | 
			
		||||
import { BannerVariant, IInternalBanner } from 'interfaces/banner';
 | 
			
		||||
import { useBanners } from 'hooks/api/getters/useBanners/useBanners';
 | 
			
		||||
import {
 | 
			
		||||
    AddOrUpdateBanner,
 | 
			
		||||
    useBannersApi,
 | 
			
		||||
} from 'hooks/api/actions/useMessageBannersApi/useMessageBannersApi';
 | 
			
		||||
import { BannerForm } from './BannerForm';
 | 
			
		||||
 | 
			
		||||
const StyledForm = styled('form')(() => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    marginTop: 'auto',
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'flex-end',
 | 
			
		||||
    paddingTop: theme.spacing(4),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
 | 
			
		||||
    marginLeft: theme.spacing(3),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IBannerModalProps {
 | 
			
		||||
    banner?: IInternalBanner;
 | 
			
		||||
    open: boolean;
 | 
			
		||||
    setOpen: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const BannerModal = ({ banner, open, setOpen }: IBannerModalProps) => {
 | 
			
		||||
    const { refetch } = useBanners();
 | 
			
		||||
    const { addBanner, updateBanner, loading } = useBannersApi();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
 | 
			
		||||
    const [enabled, setEnabled] = useState(true);
 | 
			
		||||
    const [message, setMessage] = useState('');
 | 
			
		||||
    const [variant, setVariant] = useState<BannerVariant>('info');
 | 
			
		||||
    const [sticky, setSticky] = useState(false);
 | 
			
		||||
    const [icon, setIcon] = useState('');
 | 
			
		||||
    const [link, setLink] = useState('');
 | 
			
		||||
    const [linkText, setLinkText] = useState('');
 | 
			
		||||
    const [dialogTitle, setDialogTitle] = useState('');
 | 
			
		||||
    const [dialog, setDialog] = useState('');
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setEnabled(banner?.enabled ?? true);
 | 
			
		||||
        setMessage(banner?.message || '');
 | 
			
		||||
        setVariant(banner?.variant || 'info');
 | 
			
		||||
        setSticky(banner?.sticky || false);
 | 
			
		||||
        setIcon(banner?.icon || '');
 | 
			
		||||
        setLink(banner?.link || '');
 | 
			
		||||
        setLinkText(banner?.linkText || '');
 | 
			
		||||
        setDialogTitle(banner?.dialogTitle || '');
 | 
			
		||||
        setDialog(banner?.dialog || '');
 | 
			
		||||
    }, [open, banner]);
 | 
			
		||||
 | 
			
		||||
    const editing = banner !== undefined;
 | 
			
		||||
    const title = editing ? 'Edit banner' : 'New banner';
 | 
			
		||||
    const isValid = message.length;
 | 
			
		||||
 | 
			
		||||
    const payload: AddOrUpdateBanner = {
 | 
			
		||||
        message,
 | 
			
		||||
        variant,
 | 
			
		||||
        icon,
 | 
			
		||||
        link,
 | 
			
		||||
        linkText,
 | 
			
		||||
        dialogTitle,
 | 
			
		||||
        dialog,
 | 
			
		||||
        sticky,
 | 
			
		||||
        enabled,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatApiCode = () => {
 | 
			
		||||
        return `curl --location --request ${editing ? 'PUT' : 'POST'} '${
 | 
			
		||||
            uiConfig.unleashUrl
 | 
			
		||||
        }/api/admin/banners${editing ? `/${banner.id}` : ''}' \\
 | 
			
		||||
    --header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
    --header 'Content-Type: application/json' \\
 | 
			
		||||
    --data-raw '${JSON.stringify(payload, undefined, 2)}'`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        if (!isValid) return;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (editing) {
 | 
			
		||||
                await updateBanner(banner.id, payload);
 | 
			
		||||
            } else {
 | 
			
		||||
                await addBanner(payload);
 | 
			
		||||
            }
 | 
			
		||||
            setToastData({
 | 
			
		||||
                title: `Banner ${editing ? 'updated' : 'added'} successfully`,
 | 
			
		||||
                type: 'success',
 | 
			
		||||
            });
 | 
			
		||||
            refetch();
 | 
			
		||||
            setOpen(false);
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <SidebarModal
 | 
			
		||||
            open={open}
 | 
			
		||||
            onClose={() => {
 | 
			
		||||
                setOpen(false);
 | 
			
		||||
            }}
 | 
			
		||||
            label={title}
 | 
			
		||||
        >
 | 
			
		||||
            <FormTemplate
 | 
			
		||||
                loading={loading}
 | 
			
		||||
                modal
 | 
			
		||||
                title={title}
 | 
			
		||||
                description='Banners allow you to display messages to other users inside your Unleash instance.'
 | 
			
		||||
                documentationLink='https://docs.getunleash.io/reference/banners'
 | 
			
		||||
                documentationLinkLabel='Banners documentation'
 | 
			
		||||
                formatApiCode={formatApiCode}
 | 
			
		||||
            >
 | 
			
		||||
                <StyledForm onSubmit={onSubmit}>
 | 
			
		||||
                    <BannerForm
 | 
			
		||||
                        enabled={enabled}
 | 
			
		||||
                        message={message}
 | 
			
		||||
                        variant={variant}
 | 
			
		||||
                        sticky={sticky}
 | 
			
		||||
                        icon={icon}
 | 
			
		||||
                        link={link}
 | 
			
		||||
                        linkText={linkText}
 | 
			
		||||
                        dialogTitle={dialogTitle}
 | 
			
		||||
                        dialog={dialog}
 | 
			
		||||
                        setEnabled={setEnabled}
 | 
			
		||||
                        setMessage={setMessage}
 | 
			
		||||
                        setVariant={setVariant}
 | 
			
		||||
                        setSticky={setSticky}
 | 
			
		||||
                        setIcon={setIcon}
 | 
			
		||||
                        setLink={setLink}
 | 
			
		||||
                        setLinkText={setLinkText}
 | 
			
		||||
                        setDialogTitle={setDialogTitle}
 | 
			
		||||
                        setDialog={setDialog}
 | 
			
		||||
                    />
 | 
			
		||||
                    <StyledButtonContainer>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type='submit'
 | 
			
		||||
                            variant='contained'
 | 
			
		||||
                            color='primary'
 | 
			
		||||
                            disabled={!isValid}
 | 
			
		||||
                        >
 | 
			
		||||
                            {editing ? 'Save' : 'Add'} banner
 | 
			
		||||
                        </Button>
 | 
			
		||||
                        <StyledCancelButton
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                                setOpen(false);
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            Cancel
 | 
			
		||||
                        </StyledCancelButton>
 | 
			
		||||
                    </StyledButtonContainer>
 | 
			
		||||
                </StyledForm>
 | 
			
		||||
            </FormTemplate>
 | 
			
		||||
        </SidebarModal>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -23,6 +23,7 @@ import { BannersActionsCell } from './BannersActionsCell';
 | 
			
		||||
import { BannerDeleteDialog } from './BannerDeleteDialog';
 | 
			
		||||
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
 | 
			
		||||
import omit from 'lodash.omit';
 | 
			
		||||
import { BannerModal } from '../BannerModal/BannerModal';
 | 
			
		||||
 | 
			
		||||
export const BannersTable = () => {
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
@ -234,11 +235,11 @@ export const BannersTable = () => {
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
            {/* <BannerModal
 | 
			
		||||
            <BannerModal
 | 
			
		||||
                banner={selectedBanner}
 | 
			
		||||
                open={modalOpen}
 | 
			
		||||
                setOpen={setModalOpen}
 | 
			
		||||
            /> */}
 | 
			
		||||
            />
 | 
			
		||||
            <BannerDeleteDialog
 | 
			
		||||
                banner={selectedBanner}
 | 
			
		||||
                open={deleteOpen}
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,13 @@ const StyledBar = styled('aside', {
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        padding: theme.spacing(1),
 | 
			
		||||
        gap: theme.spacing(1),
 | 
			
		||||
        borderBottom: inline ? 'none' : '1px solid',
 | 
			
		||||
        ...(inline
 | 
			
		||||
            ? {
 | 
			
		||||
                  border: '1px solid',
 | 
			
		||||
              }
 | 
			
		||||
            : {
 | 
			
		||||
                  borderBottom: '1px solid',
 | 
			
		||||
              }),
 | 
			
		||||
        borderColor: theme.palette[variant].border,
 | 
			
		||||
        background: theme.palette[variant].light,
 | 
			
		||||
        color: theme.palette[variant].dark,
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								frontend/src/component/common/FormSwitch/FormSwitch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								frontend/src/component/common/FormSwitch/FormSwitch.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
import { Box, BoxProps, FormControlLabel, Switch, styled } from '@mui/material';
 | 
			
		||||
import { Dispatch, ReactNode, SetStateAction } from 'react';
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled(Box)({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    justifyContent: 'space-between',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    width: '100%',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledSwitchSpan = styled('span')(({ theme }) => ({
 | 
			
		||||
    marginLeft: theme.spacing(0.5),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IFormSwitchProps extends BoxProps {
 | 
			
		||||
    checked: boolean;
 | 
			
		||||
    setChecked: Dispatch<SetStateAction<boolean>>;
 | 
			
		||||
    children?: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FormSwitch = ({
 | 
			
		||||
    checked,
 | 
			
		||||
    setChecked,
 | 
			
		||||
    children,
 | 
			
		||||
    ...props
 | 
			
		||||
}: IFormSwitchProps) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledContainer {...props}>
 | 
			
		||||
            {children}
 | 
			
		||||
            <FormControlLabel
 | 
			
		||||
                control={
 | 
			
		||||
                    <Switch
 | 
			
		||||
                        checked={checked}
 | 
			
		||||
                        onChange={(e) => setChecked(e.target.checked)}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
                label={
 | 
			
		||||
                    <StyledSwitchSpan>
 | 
			
		||||
                        {checked ? 'Enabled' : 'Disabled'}
 | 
			
		||||
                    </StyledSwitchSpan>
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        </StyledContainer>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -3,7 +3,7 @@ import useAPI from '../useApi/useApi';
 | 
			
		||||
 | 
			
		||||
const ENDPOINT = 'api/admin/banners';
 | 
			
		||||
 | 
			
		||||
type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
 | 
			
		||||
export type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
 | 
			
		||||
 | 
			
		||||
export const useBannersApi = () => {
 | 
			
		||||
    const { loading, makeRequest, createRequest, errors } = useAPI({
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
export type BannerVariant = 'warning' | 'info' | 'error' | 'success';
 | 
			
		||||
export type BannerVariant = 'info' | 'warning' | 'error' | 'success';
 | 
			
		||||
 | 
			
		||||
export interface IBanner {
 | 
			
		||||
    message: string;
 | 
			
		||||
@ -12,7 +12,7 @@ export interface IBanner {
 | 
			
		||||
    dialog?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IInternalBanner extends IBanner {
 | 
			
		||||
export interface IInternalBanner extends Omit<IBanner, 'plausibleEvent'> {
 | 
			
		||||
    id: number;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    createdAt: string;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user