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 { BannerDeleteDialog } from './BannerDeleteDialog';
 | 
				
			||||||
import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
 | 
					import { ToggleCell } from 'component/common/Table/cells/ToggleCell/ToggleCell';
 | 
				
			||||||
import omit from 'lodash.omit';
 | 
					import omit from 'lodash.omit';
 | 
				
			||||||
 | 
					import { BannerModal } from '../BannerModal/BannerModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BannersTable = () => {
 | 
					export const BannersTable = () => {
 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					    const { setToastData, setToastApiError } = useToast();
 | 
				
			||||||
@ -234,11 +235,11 @@ export const BannersTable = () => {
 | 
				
			|||||||
                    />
 | 
					                    />
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            {/* <BannerModal
 | 
					            <BannerModal
 | 
				
			||||||
                banner={selectedBanner}
 | 
					                banner={selectedBanner}
 | 
				
			||||||
                open={modalOpen}
 | 
					                open={modalOpen}
 | 
				
			||||||
                setOpen={setModalOpen}
 | 
					                setOpen={setModalOpen}
 | 
				
			||||||
            /> */}
 | 
					            />
 | 
				
			||||||
            <BannerDeleteDialog
 | 
					            <BannerDeleteDialog
 | 
				
			||||||
                banner={selectedBanner}
 | 
					                banner={selectedBanner}
 | 
				
			||||||
                open={deleteOpen}
 | 
					                open={deleteOpen}
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,13 @@ const StyledBar = styled('aside', {
 | 
				
			|||||||
        justifyContent: 'center',
 | 
					        justifyContent: 'center',
 | 
				
			||||||
        padding: theme.spacing(1),
 | 
					        padding: theme.spacing(1),
 | 
				
			||||||
        gap: theme.spacing(1),
 | 
					        gap: theme.spacing(1),
 | 
				
			||||||
        borderBottom: inline ? 'none' : '1px solid',
 | 
					        ...(inline
 | 
				
			||||||
 | 
					            ? {
 | 
				
			||||||
 | 
					                  border: '1px solid',
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            : {
 | 
				
			||||||
 | 
					                  borderBottom: '1px solid',
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
        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,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										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';
 | 
					const ENDPOINT = 'api/admin/banners';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
 | 
					export type AddOrUpdateBanner = Omit<IInternalBanner, 'id' | 'createdAt'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useBannersApi = () => {
 | 
					export const useBannersApi = () => {
 | 
				
			||||||
    const { loading, makeRequest, createRequest, errors } = useAPI({
 | 
					    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 {
 | 
					export interface IBanner {
 | 
				
			||||||
    message: string;
 | 
					    message: string;
 | 
				
			||||||
@ -12,7 +12,7 @@ export interface IBanner {
 | 
				
			|||||||
    dialog?: string;
 | 
					    dialog?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IInternalBanner extends IBanner {
 | 
					export interface IInternalBanner extends Omit<IBanner, 'plausibleEvent'> {
 | 
				
			||||||
    id: number;
 | 
					    id: number;
 | 
				
			||||||
    enabled: boolean;
 | 
					    enabled: boolean;
 | 
				
			||||||
    createdAt: string;
 | 
					    createdAt: string;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user