mirror of https://github.com/buster-so/buster.git
update all types to work better together
This commit is contained in:
parent
09e4b36bf5
commit
10e37f07de
|
@ -347,6 +347,7 @@ pub struct Organization {
|
|||
pub domains: Option<Vec<String>>,
|
||||
pub restrict_new_user_invitations: bool,
|
||||
pub default_role: UserOrganizationRole,
|
||||
pub organization_color_palettes: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -727,6 +728,22 @@ pub struct UserConfig {
|
|||
pub last_used_color_palette: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Organization Color Palette Types
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrganizationColorPalette {
|
||||
pub id: String,
|
||||
pub colors: Vec<String>, // Hex color codes
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrganizationColorPalettes {
|
||||
pub selected_id: Option<String>,
|
||||
pub palettes: Vec<OrganizationColorPalette>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum StepProgress {
|
||||
|
|
|
@ -452,6 +452,7 @@ diesel::table! {
|
|||
domains -> Nullable<Array<Text>>,
|
||||
restrict_new_user_invitations -> Bool,
|
||||
default_role -> UserOrganizationRoleEnum,
|
||||
organization_color_palettes -> Jsonb,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,10 @@ pub async fn post_organization_handler(name: String, user: AuthenticatedUser) ->
|
|||
domains: None,
|
||||
restrict_new_user_invitations: false,
|
||||
default_role: UserOrganizationRole::RestrictedQuerier,
|
||||
organization_color_palettes: serde_json::json!({
|
||||
"selectedId": null,
|
||||
"palettes": []
|
||||
}),
|
||||
};
|
||||
|
||||
insert_into(organizations::table)
|
||||
|
|
|
@ -123,6 +123,7 @@ pub async fn get_user_information(user_id: &Uuid) -> Result<UserInfoObject> {
|
|||
organizations::domains,
|
||||
organizations::restrict_new_user_invitations,
|
||||
organizations::default_role,
|
||||
organizations::organization_color_palettes,
|
||||
)
|
||||
.nullable(),
|
||||
users_to_organizations::role.nullable(),
|
||||
|
|
|
@ -413,6 +413,7 @@ pub async fn get_user_information(user_id: &Uuid) -> Result<UserInfoObject> {
|
|||
organizations::domains,
|
||||
organizations::restrict_new_user_invitations,
|
||||
organizations::default_role,
|
||||
organizations::organization_color_palettes,
|
||||
)
|
||||
.nullable(),
|
||||
users_to_organizations::role.nullable(),
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
"$schema": "https://turbo.build/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"dev": {}
|
||||
"dev": {
|
||||
"dependsOn": ["@buster/database#start"],
|
||||
"with": [],
|
||||
"outputs": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { OrganizationColorPalette } from '@buster/server-shared/organization';
|
||||
import type { ColorPalette, OrganizationColorPalette } from '@buster/server-shared/organization';
|
||||
|
||||
export const DEFAULT_CHART_THEME = [
|
||||
'#B399FD',
|
||||
|
@ -321,7 +321,7 @@ const PINK_THEME = [
|
|||
'#ad1457',
|
||||
];
|
||||
|
||||
export const COLORFUL_THEMES: OrganizationColorPalette[] = [
|
||||
export const COLORFUL_THEMES = [
|
||||
{
|
||||
name: 'Buster',
|
||||
colors: DEFAULT_CHART_THEME,
|
||||
|
@ -372,7 +372,7 @@ export const COLORFUL_THEMES: OrganizationColorPalette[] = [
|
|||
colors: EMERALD_SPECTRUM_THEME,
|
||||
},
|
||||
{
|
||||
name: 'Forest Lake',
|
||||
name: 'Deep Forest',
|
||||
colors: DIVERSE_DARK_PALETTE_GREEN_THEME,
|
||||
},
|
||||
{
|
||||
|
@ -384,7 +384,7 @@ export const COLORFUL_THEMES: OrganizationColorPalette[] = [
|
|||
id: theme.name,
|
||||
}));
|
||||
|
||||
export const MONOCHROME_THEMES: OrganizationColorPalette[] = [
|
||||
export const MONOCHROME_THEMES = [
|
||||
{
|
||||
name: 'Greens',
|
||||
colors: GREENS_THEME,
|
||||
|
@ -395,7 +395,7 @@ export const MONOCHROME_THEMES: OrganizationColorPalette[] = [
|
|||
colors: BLUE_TO_ORANGE_GRADIENT,
|
||||
},
|
||||
{
|
||||
name: 'Forest Lake',
|
||||
name: 'Forest Sunset',
|
||||
colors: FOREST_LAKE_GRADIENT,
|
||||
},
|
||||
{
|
||||
|
@ -435,4 +435,14 @@ export const MONOCHROME_THEMES: OrganizationColorPalette[] = [
|
|||
id: theme.name,
|
||||
}));
|
||||
|
||||
export const ALL_THEMES: OrganizationColorPalette[] = [...COLORFUL_THEMES, ...MONOCHROME_THEMES];
|
||||
const simplifyId = (name: string, index: number) => {
|
||||
return `${name.toLowerCase().replace(/ /g, '-')}-${index}`;
|
||||
};
|
||||
|
||||
export const ALL_THEMES: ColorPalette[] = [...COLORFUL_THEMES, ...MONOCHROME_THEMES].map(
|
||||
(theme, index) => ({
|
||||
colors: theme.colors,
|
||||
name: theme.name,
|
||||
id: simplifyId(theme.name, index),
|
||||
})
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Hono } from 'hono';
|
|||
import healthcheckRoutes from '../healthcheck';
|
||||
import chatsRoutes from './chats';
|
||||
import currencyRoutes from './currency';
|
||||
import dictionariesRoutes from './dictionaries';
|
||||
import electricShapeRoutes from './electric-shape';
|
||||
import organizationRoutes from './organization';
|
||||
import securityRoutes from './security';
|
||||
|
@ -19,6 +20,7 @@ const app = new Hono()
|
|||
.route('/currency', currencyRoutes)
|
||||
.route('/support', supportRoutes)
|
||||
.route('/security', securityRoutes)
|
||||
.route('/organizations', organizationRoutes);
|
||||
.route('/organizations', organizationRoutes)
|
||||
.route('/dictionaries', dictionariesRoutes);
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -63,7 +63,10 @@ export async function createTestOrganizationInDb(
|
|||
deletedAt: null,
|
||||
domain: null,
|
||||
paymentRequired: true,
|
||||
organizationColorPalettes: [],
|
||||
organizationColorPalettes: {
|
||||
selectedId: null,
|
||||
palettes: [],
|
||||
},
|
||||
...orgData,
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export const useUpdateOrganization = () => {
|
|||
queryClient.setQueryData(userQueryKey, (prev) => {
|
||||
if (!prev) return prev;
|
||||
|
||||
return create(prev, (draft) => {
|
||||
const newOrganization = create(prev, (draft) => {
|
||||
if (
|
||||
draft.organizations &&
|
||||
Array.isArray(draft.organizations) &&
|
||||
|
@ -60,6 +60,8 @@ export const useUpdateOrganization = () => {
|
|||
Object.assign(draft.organizations[0], organizationUpdates);
|
||||
}
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { DashboardLayout } from '@/layouts/DashboardLayout';
|
|||
export default async function Layout({
|
||||
children,
|
||||
params,
|
||||
...rest
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ dashboardId: string }>;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { type PropsWithChildren } from 'react';
|
||||
import React, { useRef, type PropsWithChildren } from 'react';
|
||||
import { ThemeList, type IColorTheme } from '../ThemeList';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { Plus } from '../../../ui/icons';
|
||||
|
@ -50,8 +50,7 @@ export const AddCustomThemeBase = React.memo(
|
|||
}: AddCustomThemeBaseProps) => {
|
||||
const iThemes: Required<IColorTheme>[] = customThemes.map((theme) => ({
|
||||
...theme,
|
||||
selected: theme.id === selectedThemeId,
|
||||
id: theme.id
|
||||
selected: theme.id === selectedThemeId
|
||||
}));
|
||||
|
||||
return (
|
||||
|
@ -60,12 +59,14 @@ export const AddCustomThemeBase = React.memo(
|
|||
deleteCustomTheme={deleteCustomTheme}
|
||||
modifyCustomTheme={modifyCustomTheme}>
|
||||
<div className="bg-item-select flex flex-col space-y-1 rounded border p-1">
|
||||
{iThemes.length > 0 && (
|
||||
<ThemeList
|
||||
className="border-none bg-transparent p-0"
|
||||
themes={iThemes}
|
||||
onChangeColorTheme={onSelectTheme}
|
||||
themeThreeDotsMenu={ThreeDotMenu}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AddCustomThemeButton />
|
||||
</div>
|
||||
|
@ -87,25 +88,37 @@ const ThreeDotMenu: React.FC<{ theme: IColorTheme }> = React.memo(({ theme }) =>
|
|||
await deleteCustomTheme(themeId);
|
||||
});
|
||||
|
||||
return <NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} />;
|
||||
const onUpdate = useMemoizedFn(async (theme: IColorTheme) => {
|
||||
await modifyCustomTheme(theme.id, theme);
|
||||
});
|
||||
|
||||
return (
|
||||
<NewThemePopup selectedTheme={theme} onSave={onSave} onDelete={onDelete} onUpdate={onUpdate} />
|
||||
);
|
||||
});
|
||||
|
||||
ThreeDotMenu.displayName = 'ThreeDotMenu';
|
||||
|
||||
const AddCustomThemeButton: React.FC<{}> = React.memo(({}) => {
|
||||
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: IColorTheme) => {
|
||||
await createCustomTheme(theme);
|
||||
closePopover();
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={<NewThemePopup selectedTheme={undefined} onSave={onSave} onDelete={undefined} />}
|
||||
trigger="click"
|
||||
className="p-0"
|
||||
className="max-w-[320px] p-0"
|
||||
sideOffset={12}>
|
||||
<Button variant={'ghost'} size={'tall'} prefix={<Plus />}>
|
||||
<Button ref={buttonRef} variant={'ghost'} size={'tall'} prefix={<Plus />}>
|
||||
Add a custom theme
|
||||
</Button>
|
||||
</Popover>
|
||||
|
|
|
@ -8,31 +8,34 @@ import { useColorThemes } from '@/api/buster_rest/dictionaries';
|
|||
import { StatusCard } from '@/components/ui/card/StatusCard';
|
||||
import { CircleSpinnerLoader } from '../../../ui/loaders';
|
||||
|
||||
export const DefaultThemeSelector = React.memo(() => {
|
||||
export const DefaultThemeSelector = React.memo(
|
||||
({ className, themeListClassName }: { className?: string; themeListClassName?: string }) => {
|
||||
const { data: userData } = useGetMyUserInfo();
|
||||
const { data: themes, isFetched: isFetchedThemes, isError: isErrorThemes } = useColorThemes();
|
||||
const { mutateAsync: updateOrganization } = useUpdateOrganization();
|
||||
|
||||
const organization = userData?.organizations?.[0]!;
|
||||
const organization = userData?.organizations?.[0];
|
||||
const organizationColorPalettes = organization?.organizationColorPalettes;
|
||||
|
||||
|
||||
const onCreateCustomTheme = useMemoizedFn(async (theme: IColorTheme) => {
|
||||
const currentThemeId = organization.organizationColorPalettes.selectedId;
|
||||
if (!organization) return;
|
||||
await updateOrganization({
|
||||
organizationColorPalettes: {
|
||||
selectedId: currentThemeId || theme.id,
|
||||
palettes: [...organization.organizationColorPalettes.palettes, theme]
|
||||
selectedId: theme.id,
|
||||
palettes: [theme, ...organization.organizationColorPalettes.palettes]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const onDeleteCustomTheme = useMemoizedFn(async (themeId: string) => {
|
||||
if (!organization) return;
|
||||
const currentThemeId = organization.organizationColorPalettes.selectedId;
|
||||
const isSelectedTheme = currentThemeId === themeId;
|
||||
const firstTheme = organization.organizationColorPalettes.palettes[0];
|
||||
|
||||
await updateOrganization({
|
||||
organizationColorPalettes: {
|
||||
selectedId: isSelectedTheme ? firstTheme.id : currentThemeId,
|
||||
selectedId: isSelectedTheme ? null : currentThemeId,
|
||||
palettes: organization.organizationColorPalettes.palettes.filter(
|
||||
(theme) => theme.id !== themeId
|
||||
)
|
||||
|
@ -41,6 +44,8 @@ export const DefaultThemeSelector = React.memo(() => {
|
|||
});
|
||||
|
||||
const onModifyCustomTheme = useMemoizedFn(async (themeId: string, theme: IColorTheme) => {
|
||||
if (!organization) return;
|
||||
|
||||
await updateOrganization({
|
||||
organizationColorPalettes: {
|
||||
selectedId: organization.organizationColorPalettes.selectedId,
|
||||
|
@ -52,6 +57,8 @@ export const DefaultThemeSelector = React.memo(() => {
|
|||
});
|
||||
|
||||
const onSelectTheme = useMemoizedFn((theme: IColorTheme) => {
|
||||
if (!organization) return;
|
||||
|
||||
updateOrganization({
|
||||
organizationColorPalettes: {
|
||||
selectedId: theme.id,
|
||||
|
@ -60,7 +67,8 @@ export const DefaultThemeSelector = React.memo(() => {
|
|||
});
|
||||
});
|
||||
|
||||
const organizationColorPalettes = organization?.organizationColorPalettes;
|
||||
|
||||
if (!organizationColorPalettes || !organization) return null;
|
||||
|
||||
if (!isFetchedThemes) return <CircleSpinnerLoader />;
|
||||
|
||||
|
@ -82,8 +90,10 @@ export const DefaultThemeSelector = React.memo(() => {
|
|||
onDeleteCustomTheme={onDeleteCustomTheme}
|
||||
onModifyCustomTheme={onModifyCustomTheme}
|
||||
onChangeTheme={onSelectTheme}
|
||||
themeListClassName={themeListClassName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
DefaultThemeSelector.displayName = 'DefaultThemeSelector';
|
||||
|
|
|
@ -15,7 +15,6 @@ const meta: Meta<typeof DefaultThemeSelectorBase> = {
|
|||
onDeleteCustomTheme: fn(),
|
||||
onModifyCustomTheme: fn(),
|
||||
selectedThemeId: 'custom-sunset',
|
||||
useDefaultThemes: true,
|
||||
customThemes: [
|
||||
{
|
||||
name: 'Custom Sunset',
|
||||
|
|
|
@ -3,7 +3,6 @@ import type { IColorTheme } from '../ThemeList/interfaces';
|
|||
import { ThemeList } from '../ThemeList';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { AddCustomThemeBase } from './AddCustomThemeBase';
|
||||
import { useColorThemes } from '../../../../api/buster_rest/dictionaries';
|
||||
|
||||
export interface DefaultThemeSelectorProps {
|
||||
customThemes: Omit<IColorTheme, 'selected'>[];
|
||||
|
@ -13,31 +12,29 @@ export interface DefaultThemeSelectorProps {
|
|||
onDeleteCustomTheme: (themeId: string) => Promise<void>;
|
||||
onModifyCustomTheme: (themeId: string, theme: IColorTheme) => Promise<void>;
|
||||
selectedThemeId: string | null;
|
||||
useDefaultThemes?: boolean;
|
||||
themeListClassName?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DefaultThemeSelectorBase = React.memo(
|
||||
({
|
||||
customThemes,
|
||||
themes,
|
||||
useDefaultThemes = true,
|
||||
selectedThemeId,
|
||||
onChangeTheme,
|
||||
themeListClassName,
|
||||
className,
|
||||
onCreateCustomTheme,
|
||||
onDeleteCustomTheme,
|
||||
onModifyCustomTheme
|
||||
}: DefaultThemeSelectorProps) => {
|
||||
const iThemes: Required<IColorTheme>[] = themes?.map((theme) => ({
|
||||
...theme,
|
||||
selected: theme.id === selectedThemeId,
|
||||
id: theme.name
|
||||
selected: theme.id === selectedThemeId
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col space-y-2.5">
|
||||
<div>
|
||||
<div className={cn('flex w-full flex-col space-y-2.5', className)}>
|
||||
<AddCustomThemeBase
|
||||
customThemes={customThemes}
|
||||
selectedThemeId={selectedThemeId}
|
||||
|
@ -46,9 +43,13 @@ export const DefaultThemeSelectorBase = React.memo(
|
|||
deleteCustomTheme={onDeleteCustomTheme}
|
||||
modifyCustomTheme={onModifyCustomTheme}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(themeListClassName)}>
|
||||
<ThemeList themes={iThemes} onChangeColorTheme={onChangeTheme} />
|
||||
|
||||
<div>
|
||||
<ThemeList
|
||||
themes={iThemes}
|
||||
onChangeColorTheme={onChangeTheme}
|
||||
className={cn(themeListClassName)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { AppTooltip } from '@/components/ui/tooltip';
|
|||
import { ColorPicker } from '@/components/ui/color-picker';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMemoizedFn } from '@/hooks';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
|
@ -166,13 +166,21 @@ const ColorWithPicker: React.FC<{
|
|||
const originalColor = useRef(colorProp);
|
||||
const [color, setColor] = useState(colorProp);
|
||||
|
||||
useEffect(() => {
|
||||
setColor(colorProp);
|
||||
originalColor.current = colorProp;
|
||||
}, [colorProp]);
|
||||
|
||||
return (
|
||||
<ColorPicker
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
align="center"
|
||||
side="bottom"
|
||||
onOpenChange={onPickerOpenChange}
|
||||
onOpenChange={(v) => {
|
||||
setColor(originalColor.current);
|
||||
onPickerOpenChange(v);
|
||||
}}
|
||||
popoverChildren={
|
||||
<div className="flex w-full items-center gap-2 border-t py-2">
|
||||
<Button block variant={'default'} onClick={() => setColor(originalColor.current)}>
|
||||
|
|
|
@ -14,10 +14,11 @@ interface NewThemePopupProps {
|
|||
selectedTheme?: IColorTheme;
|
||||
onSave: (theme: IColorTheme) => Promise<void>;
|
||||
onDelete?: (id: string) => Promise<void>;
|
||||
onUpdate?: (theme: IColorTheme) => Promise<void>;
|
||||
}
|
||||
|
||||
export const NewThemePopup = React.memo(
|
||||
({ selectedTheme, onDelete, onSave }: NewThemePopupProps) => {
|
||||
({ selectedTheme, onDelete, onUpdate, onSave }: NewThemePopupProps) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [colors, setColors] = useState<string[]>(DEFAULT_CHART_THEME);
|
||||
const [id, setId] = useState(uuidv4());
|
||||
|
@ -45,6 +46,13 @@ export const NewThemePopup = React.memo(
|
|||
}, 350);
|
||||
});
|
||||
|
||||
const onUpdateClick = useMemoizedFn(async () => {
|
||||
await onUpdate?.({ id, name: title, colors });
|
||||
setTimeout(() => {
|
||||
reset();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTheme) {
|
||||
setTitle(selectedTheme.name);
|
||||
|
@ -54,7 +62,7 @@ export const NewThemePopup = React.memo(
|
|||
}, [selectedTheme]);
|
||||
|
||||
return (
|
||||
<div className="w-[280px]">
|
||||
<div className="w-[280px] max-w-[340px]">
|
||||
<div className="grid grid-cols-[80px_1fr] items-center gap-2 p-2.5">
|
||||
<Text>Title</Text>
|
||||
<Input
|
||||
|
@ -67,13 +75,22 @@ export const NewThemePopup = React.memo(
|
|||
</div>
|
||||
<div className="w-full border-t"></div>
|
||||
|
||||
<div className="p-2.5">
|
||||
<div className="flex space-x-1.5 p-2.5">
|
||||
{onDelete && !isNewTheme && (
|
||||
<Button
|
||||
block
|
||||
disabled={disableCreateTheme}
|
||||
onClick={isNewTheme ? onSaveClick : onDeleteClick}
|
||||
prefix={isNewTheme ? <Plus /> : <Trash />}>
|
||||
{isNewTheme ? 'Create theme' : 'Delete theme'}
|
||||
prefix={<Trash />}>
|
||||
{'Delete theme'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
block
|
||||
disabled={disableCreateTheme}
|
||||
onClick={isNewTheme ? onSaveClick : onUpdateClick}
|
||||
prefix={<Plus />}>
|
||||
{isNewTheme || !onUpdate ? 'Create theme' : 'Update theme'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from './DefaultThemeSelectorBase';
|
||||
export * from './DefaultThemeSelector';
|
||||
|
|
|
@ -2,10 +2,10 @@ import type React from 'react';
|
|||
import { cn } from '@/lib/classMerge';
|
||||
|
||||
export const ThemeColorDots: React.FC<{
|
||||
selected: boolean;
|
||||
selected?: boolean;
|
||||
colors: string[];
|
||||
numberOfColors?: number | 'all';
|
||||
}> = ({ selected, colors, numberOfColors = 'all' }) => {
|
||||
}> = ({ selected = false, colors, numberOfColors = 'all' }) => {
|
||||
const numberOfColorsToShow = numberOfColors === 'all' ? colors.length : numberOfColors;
|
||||
|
||||
return (
|
||||
|
@ -14,8 +14,7 @@ export const ThemeColorDots: React.FC<{
|
|||
<div
|
||||
key={color + colorIdx}
|
||||
className={cn(
|
||||
'ball rounded-full',
|
||||
colorIdx > 0 && '-ml-0.5 h-2 w-2 shadow-[0_0_0_0.75px]',
|
||||
'ball -ml-0.5 h-2 w-2 rounded-full shadow-[0_0_0_0.75px]',
|
||||
!selected ? 'shadow-item-select' : 'shadow-background'
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
|
|
|
@ -22,7 +22,7 @@ export const ThemeList: React.FC<{
|
|||
)}>
|
||||
{themes.map((theme) => (
|
||||
<ColorOption
|
||||
key={theme.name}
|
||||
key={theme.id}
|
||||
theme={theme}
|
||||
selected={theme.selected}
|
||||
onChangeColorTheme={onChangeColorTheme}
|
||||
|
@ -52,7 +52,7 @@ const ColorOption: React.FC<{
|
|||
data-selected={selected}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between space-x-2.5 overflow-hidden',
|
||||
'h-7 cursor-pointer rounded-sm px-3 py-2',
|
||||
'h-7 min-h-7 cursor-pointer rounded-sm px-3 py-2',
|
||||
selected ? 'bg-background border' : 'bg-item-active hover:bg-nav-item-hover'
|
||||
)}>
|
||||
<Text truncate variant={selected ? 'default' : 'secondary'}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { OrganizationColorPalette } from '@buster/server-shared/organization';
|
||||
import type { ColorPalette } from '@buster/server-shared/organization';
|
||||
|
||||
export type IColorTheme = OrganizationColorPalette & {
|
||||
export type IColorTheme = ColorPalette & {
|
||||
selected?: boolean;
|
||||
};
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { SettingsCards } from './SettingsCard';
|
||||
import { Text } from '@/components/ui/typography';
|
||||
import { useGetMyUserInfo } from '@/api/buster_rest/users/queryRequests';
|
||||
import { useMount } from '../../../hooks';
|
||||
import { prefetchColorThemes, useColorThemes } from '../../../api/buster_rest/dictionaries';
|
||||
import { ThemeColorDots } from '../colors/ThemeList/ThemeColorDots';
|
||||
import { Popover } from '../../ui/popover';
|
||||
import { DefaultThemeSelector } from '../colors/DefaultThemeSelector';
|
||||
import { ChevronDown } from '../../ui/icons';
|
||||
|
||||
export const DefaultColorThemeCard = React.memo(() => {
|
||||
const { data: userData } = useGetMyUserInfo();
|
||||
|
||||
const organization = userData?.organizations?.[0]!;
|
||||
|
||||
const defaultColorTheme = organization.organizationColorPalettes?.selectedId;
|
||||
|
||||
return (
|
||||
<SettingsCards
|
||||
cards={[
|
||||
{
|
||||
sections: [
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<div key="default-color-theme" className="flex items-center justify-between space-x-4">
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<Text>Default color theme</Text>
|
||||
<Text variant="secondary" size={'xs'}>
|
||||
Default color theme that Buster will use when creating charts
|
||||
</Text>
|
||||
</div>
|
||||
<div>PICKER</div>
|
||||
<PickButton />
|
||||
</div>
|
||||
]
|
||||
}
|
||||
|
@ -34,3 +34,55 @@ export const DefaultColorThemeCard = React.memo(() => {
|
|||
});
|
||||
|
||||
DefaultColorThemeCard.displayName = 'DefaultColorThemeCard';
|
||||
|
||||
const PickButton = React.memo(() => {
|
||||
const { data: userData } = useGetMyUserInfo();
|
||||
const { data: colorThemes } = useColorThemes();
|
||||
|
||||
const organization = userData?.organizations?.[0];
|
||||
const customThemes = organization?.organizationColorPalettes.palettes ?? [];
|
||||
const defaultColorThemeId = organization?.organizationColorPalettes.selectedId;
|
||||
|
||||
const allThemes = useMemo(() => {
|
||||
return [...colorThemes, ...customThemes];
|
||||
}, [colorThemes, customThemes]);
|
||||
|
||||
const defaultColorTheme = useMemo(() => {
|
||||
return allThemes.find((theme) => theme.id === defaultColorThemeId);
|
||||
}, [allThemes, defaultColorThemeId]);
|
||||
|
||||
const hasDefaultColorTheme = !!defaultColorTheme;
|
||||
|
||||
useMount(() => {
|
||||
prefetchColorThemes();
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="p-0"
|
||||
align="end"
|
||||
content={
|
||||
<div className="max-w-[320px] overflow-y-auto p-2.5">
|
||||
<DefaultThemeSelector themeListClassName="max-h-[320px] h-full overflow-y-auto" />
|
||||
</div>
|
||||
}>
|
||||
<div className="hover:bg-item-hover flex h-7 min-h-7 cursor-pointer items-center space-x-1.5 overflow-hidden rounded border px-2 py-1 pl-2.5">
|
||||
<div>
|
||||
{hasDefaultColorTheme ? (
|
||||
<ThemeColorDots colors={defaultColorTheme.colors} numberOfColors={'all'} />
|
||||
) : (
|
||||
<Text variant="secondary" size={'xs'}>
|
||||
No default color theme
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-icon-color flex items-center justify-center">
|
||||
<ChevronDown />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
PickButton.displayName = 'PickButton';
|
||||
|
|
|
@ -12,6 +12,8 @@ const Popover = PopoverPrimitive.Root;
|
|||
|
||||
interface PopoverProps extends React.ComponentPropsWithoutRef<typeof Popover> {
|
||||
trigger?: PopoverTriggerType;
|
||||
children: React.ReactNode;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
const PopoverRoot: React.FC<PopoverProps> = ({ children, trigger = 'click', ...props }) => {
|
||||
|
|
|
@ -27,6 +27,10 @@ export const MetricStylingApp: React.FC<{
|
|||
const { data: chartConfig } = useGetMetric({ id: metricId }, { select: (x) => x.chart_config });
|
||||
const { data: metricData } = useGetMetricData({ id: metricId }, { enabled: false });
|
||||
|
||||
useMount(() => {
|
||||
prefetchColorThemes();
|
||||
});
|
||||
|
||||
if (!chartConfig) return null;
|
||||
|
||||
const columnMetadata = metricData?.data_metadata?.column_metadata || [];
|
||||
|
@ -92,9 +96,7 @@ export const MetricStylingApp: React.FC<{
|
|||
barAndLineAxis
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
prefetchColorThemes();
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-hidden pt-3">
|
||||
|
|
|
@ -23,7 +23,7 @@ const OrganizationColorPaletteSchema = z.object({
|
|||
const UpdateOrganizationInputSchema = z.object({
|
||||
organizationId: z.string().uuid('Organization ID must be a valid UUID'),
|
||||
organizationColorPalettes: z.object({
|
||||
selectedId: z.string().min(1).max(255),
|
||||
selectedId: z.string().min(1).max(255).nullable(),
|
||||
palettes: z.array(OrganizationColorPaletteSchema).refine(
|
||||
(palettes) => {
|
||||
if (!palettes || palettes.length === 0) return true;
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import type { OrganizationColorPalette } from '../organization/organization.types';
|
||||
import type { ColorPalette } from '../organization/organization.types';
|
||||
|
||||
export type ColorThemeDictionariesResponse = OrganizationColorPalette[];
|
||||
export type ColorThemeDictionariesResponse = ColorPalette[];
|
||||
|
|
|
@ -11,12 +11,17 @@ const HexColorSchema = z
|
|||
'Must be a valid 3 or 6 digit hex color code (e.g., #fff or #ffffff)'
|
||||
);
|
||||
|
||||
export const OrganizationColorPaletteSchema = z.object({
|
||||
export const ColorPalettesSchema = z.object({
|
||||
id: z.string(),
|
||||
colors: z.array(HexColorSchema).min(1).max(25),
|
||||
name: z.string().min(1).max(255),
|
||||
});
|
||||
|
||||
export const OrganizationColorPaletteSchema = z.object({
|
||||
selectedId: z.string().nullable(),
|
||||
palettes: z.array(ColorPalettesSchema),
|
||||
});
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
|
@ -28,13 +33,11 @@ export const OrganizationSchema = z.object({
|
|||
domains: z.array(z.string()).nullable(),
|
||||
restrictNewUserInvitations: z.boolean(),
|
||||
defaultRole: OrganizationRoleSchema,
|
||||
organizationColorPalettes: z.object({
|
||||
selectedId: z.string(),
|
||||
palettes: z.array(OrganizationColorPaletteSchema),
|
||||
}),
|
||||
organizationColorPalettes: OrganizationColorPaletteSchema,
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof OrganizationSchema>;
|
||||
export type OrganizationColorPalette = z.infer<typeof OrganizationColorPaletteSchema>;
|
||||
export type ColorPalette = z.infer<typeof ColorPalettesSchema>;
|
||||
|
||||
type _OrganizationEqualityCheck = Expect<Equal<Organization, typeof organizations.$inferSelect>>;
|
||||
|
|
|
@ -3,10 +3,7 @@ import { OrganizationColorPaletteSchema } from './organization.types';
|
|||
|
||||
// Update Organization Request/Response Types
|
||||
export const UpdateOrganizationRequestSchema = z.object({
|
||||
organizationColorPalettes: z.object({
|
||||
selectedId: z.string(),
|
||||
palettes: z.array(OrganizationColorPaletteSchema),
|
||||
}),
|
||||
organizationColorPalettes: OrganizationColorPaletteSchema,
|
||||
});
|
||||
|
||||
export type UpdateOrganizationRequest = z.infer<typeof UpdateOrganizationRequestSchema>;
|
||||
|
|
Loading…
Reference in New Issue