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

View File

@ -27,5 +27,11 @@ export const AddThemeProviderWrapper: React.FC<PropsWithChildren<AddThemeProps>>
};
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,27 +1,30 @@
import React from 'react';
import React, { type PropsWithChildren } from 'react';
import type { IColorPalette } from '../ThemeList';
import { useMemoizedFn } from '@/hooks';
import { NewThemePopup } from './NewThemePopup';
import { useAddTheme } from './AddThemeProviderWrapper';
export const EditCustomThemeMenu: React.FC<{ theme: IColorPalette }> = React.memo(({ theme }) => {
const { deleteCustomTheme, modifyCustomTheme } = useAddTheme();
export const EditCustomThemeMenu: React.FC<PropsWithChildren<{ theme: IColorPalette }>> =
React.memo(({ theme, children }) => {
const { deleteCustomTheme, modifyCustomTheme } = useAddTheme();
const onSave = useMemoizedFn(async (theme: IColorPalette) => {
await modifyCustomTheme(theme.id, theme);
const onSave = useMemoizedFn((theme: IColorPalette) => {
return modifyCustomTheme(theme.id, theme);
});
const onDelete = useMemoizedFn((themeId: string) => {
return deleteCustomTheme(themeId);
});
const onUpdate = useMemoizedFn((theme: IColorPalette) => {
return modifyCustomTheme(theme.id, theme);
});
return (
<NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} onUpdate={onUpdate}>
{children}
</NewThemePopup>
);
});
const onDelete = useMemoizedFn(async (themeId: string) => {
await deleteCustomTheme(themeId);
});
const onUpdate = useMemoizedFn(async (theme: IColorPalette) => {
await modifyCustomTheme(theme.id, theme);
});
return (
<NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} onUpdate={onUpdate} />
);
});
EditCustomThemeMenu.displayName = 'EditCustomThemeMenu';

View File

@ -1,14 +1,15 @@
import { Button } from '@/components/ui/buttons';
import { Input } from '@/components/ui/inputs';
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 { useMemoizedFn } from '@/hooks';
import { v4 as uuidv4 } from 'uuid';
import { Plus, Trash } from '../../../ui/icons';
import { Plus, Trash, FloppyDisk } from '../../../ui/icons';
import { ColorPickButton } from './DraggableColorPicker';
import { inputHasText } from '@/lib/text';
import { DEFAULT_CHART_THEME } from '@buster/server-shared/metrics';
import { Popover } from '../../../ui/popover';
interface NewThemePopupProps {
selectedTheme?: IColorPalette;
@ -17,8 +18,16 @@ interface NewThemePopupProps {
onUpdate?: (theme: IColorPalette) => Promise<void>;
}
export const NewThemePopup = React.memo(
({ selectedTheme, onDelete, onUpdate, onSave }: NewThemePopupProps) => {
const NewThemePopupContent = React.memo(
({
selectedTheme,
onDelete,
onUpdate,
onSave,
triggerRef
}: NewThemePopupProps & {
triggerRef: React.RefObject<HTMLSpanElement>;
}) => {
const [title, setTitle] = useState('');
const [colors, setColors] = useState<string[]>(DEFAULT_CHART_THEME);
const [id, setId] = useState(uuidv4());
@ -32,8 +41,13 @@ export const NewThemePopup = React.memo(
setId(uuidv4());
});
const closePopover = useMemoizedFn(() => {
triggerRef.current?.click();
});
const onDeleteClick = useMemoizedFn(async () => {
if (selectedTheme) await onDelete?.(id);
closePopover();
setTimeout(() => {
reset();
}, 350);
@ -41,6 +55,7 @@ export const NewThemePopup = React.memo(
const onSaveClick = useMemoizedFn(async () => {
await onSave({ id, name: title, colors });
closePopover();
setTimeout(() => {
reset();
}, 350);
@ -48,6 +63,7 @@ export const NewThemePopup = React.memo(
const onUpdateClick = useMemoizedFn(async () => {
await onUpdate?.({ id, name: title, colors });
closePopover();
setTimeout(() => {
reset();
}, 350);
@ -89,7 +105,7 @@ export const NewThemePopup = React.memo(
block
disabled={disableCreateTheme}
onClick={isNewTheme ? onSaveClick : onUpdateClick}
prefix={<Plus />}>
prefix={isNewTheme ? <Plus /> : <FloppyDisk />}>
{isNewTheme || !onUpdate ? 'Create theme' : 'Update theme'}
</Button>
</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[];
className?: string;
onChangeColorTheme: (theme: IColorPalette) => void;
themeThreeDotsMenu?: React.FC<{ theme: IColorPalette }>;
themeThreeDotsMenu?: React.FC<{ theme: IColorPalette; children: React.ReactNode }>;
}> = ({ themes, className, themeThreeDotsMenu, onChangeColorTheme }) => {
return (
<div
@ -36,7 +36,7 @@ export const ThemeList: React.FC<{
const ColorOption: React.FC<{
theme: IColorPalette;
selected: boolean | undefined;
threeDotMenu?: React.FC<{ theme: IColorPalette }>;
threeDotMenu?: React.FC<{ theme: IColorPalette; children: React.ReactNode }>;
onChangeColorTheme: (theme: IColorPalette) => void;
}> = React.memo(({ theme, selected = false, threeDotMenu, onChangeColorTheme }) => {
const { name, colors } = theme;
@ -65,17 +65,14 @@ const ColorOption: React.FC<{
{shouldShowMenu && (
<div onClick={(e) => e.stopPropagation()}>
<Popover
className="p-0"
content={<ThreeDotMenuComponent theme={theme} />}
trigger="click">
<ThreeDotMenuComponent theme={theme}>
<Button
data-testid={`color-theme-three-dots-menu`}
variant={'ghost'}
size={'small'}
prefix={<Dots />}
/>
</Popover>
</ThreeDotMenuComponent>
</div>
)}
</div>

View File

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