Merge pull request #554 from buster-so/big-nate/bus-1424-default-color-palette-in-workspace-settings

default color palette in workspace settings
This commit is contained in:
Nate Kelley 2025-07-18 14:13:06 -06:00 committed by GitHub
commit 0c345cb00e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 81 additions and 51 deletions

View File

@ -1,10 +1,9 @@
import React, { useRef, type PropsWithChildren } from 'react'; import React, { useRef } from 'react';
import { ThemeList, type IColorPalette } from '../ThemeList'; import { ThemeList, type IColorPalette } from '../ThemeList';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Plus } from '../../../ui/icons'; import { Plus } from '../../../ui/icons';
import { NewThemePopup } from './NewThemePopup'; import { NewThemePopup } from './NewThemePopup';
import { useMemoizedFn } from '@/hooks/useMemoizedFn'; import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { Popover } from '../../../ui/popover';
import { EditCustomThemeMenu } from './EditCustomThemeMenu'; import { EditCustomThemeMenu } from './EditCustomThemeMenu';
import { AddThemeProviderWrapper, useAddTheme } from './AddThemeProviderWrapper'; import { AddThemeProviderWrapper, useAddTheme } from './AddThemeProviderWrapper';
@ -59,25 +58,16 @@ const AddCustomThemeButton: React.FC = React.memo(({}) => {
const { createCustomTheme } = useAddTheme(); const { createCustomTheme } = useAddTheme();
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const closePopover = useMemoizedFn(() => {
buttonRef.current?.click();
});
const onSave = useMemoizedFn(async (theme: IColorPalette) => {
await createCustomTheme(theme);
closePopover();
});
return ( return (
<Popover <NewThemePopup
content={<NewThemePopup selectedTheme={undefined} onSave={onSave} onDelete={undefined} />} onSave={createCustomTheme}
trigger="click" selectedTheme={undefined}
className="max-w-[320px] p-0" onDelete={undefined}
sideOffset={12}> onUpdate={undefined}>
<Button ref={buttonRef} variant={'ghost'} size={'tall'} prefix={<Plus />}> <Button ref={buttonRef} variant={'ghost'} size={'tall'} prefix={<Plus />}>
Add a custom theme Add a custom theme
</Button> </Button>
</Popover> </NewThemePopup>
); );
}); });

View File

@ -27,5 +27,11 @@ export const AddThemeProviderWrapper: React.FC<PropsWithChildren<AddThemeProps>>
}; };
export const useAddTheme = () => { export const useAddTheme = () => {
return React.useContext(AddThemeProvider); const context = React.useContext(AddThemeProvider);
if (!context) {
throw new Error('useAddTheme must be used within an AddThemeProvider');
}
return context;
}; };

View File

@ -1,26 +1,29 @@
import React from 'react'; import React, { type PropsWithChildren } from 'react';
import type { IColorPalette } from '../ThemeList'; import type { IColorPalette } from '../ThemeList';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { NewThemePopup } from './NewThemePopup'; import { NewThemePopup } from './NewThemePopup';
import { useAddTheme } from './AddThemeProviderWrapper'; import { useAddTheme } from './AddThemeProviderWrapper';
export const EditCustomThemeMenu: React.FC<{ theme: IColorPalette }> = React.memo(({ theme }) => { export const EditCustomThemeMenu: React.FC<PropsWithChildren<{ theme: IColorPalette }>> =
React.memo(({ theme, children }) => {
const { deleteCustomTheme, modifyCustomTheme } = useAddTheme(); const { deleteCustomTheme, modifyCustomTheme } = useAddTheme();
const onSave = useMemoizedFn(async (theme: IColorPalette) => { const onSave = useMemoizedFn((theme: IColorPalette) => {
await modifyCustomTheme(theme.id, theme); return modifyCustomTheme(theme.id, theme);
}); });
const onDelete = useMemoizedFn(async (themeId: string) => { const onDelete = useMemoizedFn((themeId: string) => {
await deleteCustomTheme(themeId); return deleteCustomTheme(themeId);
}); });
const onUpdate = useMemoizedFn(async (theme: IColorPalette) => { const onUpdate = useMemoizedFn((theme: IColorPalette) => {
await modifyCustomTheme(theme.id, theme); return modifyCustomTheme(theme.id, theme);
}); });
return ( return (
<NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} onUpdate={onUpdate} /> <NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} onUpdate={onUpdate}>
{children}
</NewThemePopup>
); );
}); });

View File

@ -1,14 +1,15 @@
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Input } from '@/components/ui/inputs'; import { Input } from '@/components/ui/inputs';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { IColorPalette } from '../ThemeList'; import type { IColorPalette } from '../ThemeList';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Plus, Trash } from '../../../ui/icons'; import { Plus, Trash, FloppyDisk } from '../../../ui/icons';
import { ColorPickButton } from './DraggableColorPicker'; import { ColorPickButton } from './DraggableColorPicker';
import { inputHasText } from '@/lib/text'; import { inputHasText } from '@/lib/text';
import { DEFAULT_CHART_THEME } from '@buster/server-shared/metrics'; import { DEFAULT_CHART_THEME } from '@buster/server-shared/metrics';
import { Popover } from '../../../ui/popover';
interface NewThemePopupProps { interface NewThemePopupProps {
selectedTheme?: IColorPalette; selectedTheme?: IColorPalette;
@ -17,8 +18,16 @@ interface NewThemePopupProps {
onUpdate?: (theme: IColorPalette) => Promise<void>; onUpdate?: (theme: IColorPalette) => Promise<void>;
} }
export const NewThemePopup = React.memo( const NewThemePopupContent = React.memo(
({ selectedTheme, onDelete, onUpdate, onSave }: NewThemePopupProps) => { ({
selectedTheme,
onDelete,
onUpdate,
onSave,
triggerRef
}: NewThemePopupProps & {
triggerRef: React.RefObject<HTMLSpanElement>;
}) => {
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [colors, setColors] = useState<string[]>(DEFAULT_CHART_THEME); const [colors, setColors] = useState<string[]>(DEFAULT_CHART_THEME);
const [id, setId] = useState(uuidv4()); const [id, setId] = useState(uuidv4());
@ -32,8 +41,13 @@ export const NewThemePopup = React.memo(
setId(uuidv4()); setId(uuidv4());
}); });
const closePopover = useMemoizedFn(() => {
triggerRef.current?.click();
});
const onDeleteClick = useMemoizedFn(async () => { const onDeleteClick = useMemoizedFn(async () => {
if (selectedTheme) await onDelete?.(id); if (selectedTheme) await onDelete?.(id);
closePopover();
setTimeout(() => { setTimeout(() => {
reset(); reset();
}, 350); }, 350);
@ -41,6 +55,7 @@ export const NewThemePopup = React.memo(
const onSaveClick = useMemoizedFn(async () => { const onSaveClick = useMemoizedFn(async () => {
await onSave({ id, name: title, colors }); await onSave({ id, name: title, colors });
closePopover();
setTimeout(() => { setTimeout(() => {
reset(); reset();
}, 350); }, 350);
@ -48,6 +63,7 @@ export const NewThemePopup = React.memo(
const onUpdateClick = useMemoizedFn(async () => { const onUpdateClick = useMemoizedFn(async () => {
await onUpdate?.({ id, name: title, colors }); await onUpdate?.({ id, name: title, colors });
closePopover();
setTimeout(() => { setTimeout(() => {
reset(); reset();
}, 350); }, 350);
@ -89,7 +105,7 @@ export const NewThemePopup = React.memo(
block block
disabled={disableCreateTheme} disabled={disableCreateTheme}
onClick={isNewTheme ? onSaveClick : onUpdateClick} onClick={isNewTheme ? onSaveClick : onUpdateClick}
prefix={<Plus />}> prefix={isNewTheme ? <Plus /> : <FloppyDisk />}>
{isNewTheme || !onUpdate ? 'Create theme' : 'Update theme'} {isNewTheme || !onUpdate ? 'Create theme' : 'Update theme'}
</Button> </Button>
</div> </div>
@ -98,4 +114,22 @@ export const NewThemePopup = React.memo(
} }
); );
NewThemePopup.displayName = 'NewThemePopup'; NewThemePopupContent.displayName = 'NewThemePopupContent';
export const NewThemePopup = ({
children,
...props
}: NewThemePopupProps & { children: React.ReactNode }) => {
const triggerRef = useRef<HTMLSpanElement>(null);
return (
<Popover
content={<NewThemePopupContent {...props} triggerRef={triggerRef} />}
trigger="click"
className="max-w-[320px] p-0"
sideOffset={12}>
<span data-testid="new-theme-popup-trigger" ref={triggerRef}>
{children}
</span>
</Popover>
);
};

View File

@ -11,7 +11,7 @@ export const ThemeList: React.FC<{
themes: IColorPalette[]; themes: IColorPalette[];
className?: string; className?: string;
onChangeColorTheme: (theme: IColorPalette) => void; onChangeColorTheme: (theme: IColorPalette) => void;
themeThreeDotsMenu?: React.FC<{ theme: IColorPalette }>; themeThreeDotsMenu?: React.FC<{ theme: IColorPalette; children: React.ReactNode }>;
}> = ({ themes, className, themeThreeDotsMenu, onChangeColorTheme }) => { }> = ({ themes, className, themeThreeDotsMenu, onChangeColorTheme }) => {
return ( return (
<div <div
@ -36,7 +36,7 @@ export const ThemeList: React.FC<{
const ColorOption: React.FC<{ const ColorOption: React.FC<{
theme: IColorPalette; theme: IColorPalette;
selected: boolean | undefined; selected: boolean | undefined;
threeDotMenu?: React.FC<{ theme: IColorPalette }>; threeDotMenu?: React.FC<{ theme: IColorPalette; children: React.ReactNode }>;
onChangeColorTheme: (theme: IColorPalette) => void; onChangeColorTheme: (theme: IColorPalette) => void;
}> = React.memo(({ theme, selected = false, threeDotMenu, onChangeColorTheme }) => { }> = React.memo(({ theme, selected = false, threeDotMenu, onChangeColorTheme }) => {
const { name, colors } = theme; const { name, colors } = theme;
@ -65,17 +65,14 @@ const ColorOption: React.FC<{
{shouldShowMenu && ( {shouldShowMenu && (
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Popover <ThreeDotMenuComponent theme={theme}>
className="p-0"
content={<ThreeDotMenuComponent theme={theme} />}
trigger="click">
<Button <Button
data-testid={`color-theme-three-dots-menu`} data-testid={`color-theme-three-dots-menu`}
variant={'ghost'} variant={'ghost'}
size={'small'} size={'small'}
prefix={<Dots />} prefix={<Dots />}
/> />
</Popover> </ThreeDotMenuComponent>
</div> </div>
)} )}
</div> </div>

View File

@ -22,12 +22,12 @@ export const StylingAppColors: React.FC<{
}); });
return ( return (
<div className="mt-3 flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{/* <div className={className}> {/* <div className={className}>
<SelectColorApp selectedTab={selectedTab} onChange={setSelectedTab} /> <SelectColorApp selectedTab={selectedTab} onChange={setSelectedTab} />
</div> */} </div> */}
<div className={cn(className, 'mb-12')}> <div className={cn(className, 'mt-3 mb-12')}>
<AnimatePresence mode="wait" initial={false}> <AnimatePresence mode="wait" initial={false}>
<motion.div <motion.div
key={selectedTab} key={selectedTab}