diff --git a/apps/api/libs/database/src/models.rs b/apps/api/libs/database/src/models.rs index 20aa970f3..0f16a58ff 100644 --- a/apps/api/libs/database/src/models.rs +++ b/apps/api/libs/database/src/models.rs @@ -347,6 +347,7 @@ pub struct Organization { pub domains: Option>, 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>, } +/// Organization Color Palette Types +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationColorPalette { + pub id: String, + pub colors: Vec, // Hex color codes + pub name: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct OrganizationColorPalettes { + pub selected_id: Option, + pub palettes: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub enum StepProgress { diff --git a/apps/api/libs/database/src/schema.rs b/apps/api/libs/database/src/schema.rs index af499ceca..e49798f2a 100644 --- a/apps/api/libs/database/src/schema.rs +++ b/apps/api/libs/database/src/schema.rs @@ -452,6 +452,7 @@ diesel::table! { domains -> Nullable>, restrict_new_user_invitations -> Bool, default_role -> UserOrganizationRoleEnum, + organization_color_palettes -> Jsonb, } } diff --git a/apps/api/libs/handlers/src/organizations/post_organization_handler.rs b/apps/api/libs/handlers/src/organizations/post_organization_handler.rs index 62dbd970d..9e8105f79 100644 --- a/apps/api/libs/handlers/src/organizations/post_organization_handler.rs +++ b/apps/api/libs/handlers/src/organizations/post_organization_handler.rs @@ -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) diff --git a/apps/api/server/src/routes/rest/routes/users/get_user.rs b/apps/api/server/src/routes/rest/routes/users/get_user.rs index df8d4bf64..efe0aa453 100644 --- a/apps/api/server/src/routes/rest/routes/users/get_user.rs +++ b/apps/api/server/src/routes/rest/routes/users/get_user.rs @@ -123,6 +123,7 @@ pub async fn get_user_information(user_id: &Uuid) -> Result { organizations::domains, organizations::restrict_new_user_invitations, organizations::default_role, + organizations::organization_color_palettes, ) .nullable(), users_to_organizations::role.nullable(), diff --git a/apps/api/server/src/routes/ws/ws_utils.rs b/apps/api/server/src/routes/ws/ws_utils.rs index 8f2d54b27..7eb3750cd 100644 --- a/apps/api/server/src/routes/ws/ws_utils.rs +++ b/apps/api/server/src/routes/ws/ws_utils.rs @@ -413,6 +413,7 @@ pub async fn get_user_information(user_id: &Uuid) -> Result { organizations::domains, organizations::restrict_new_user_invitations, organizations::default_role, + organizations::organization_color_palettes, ) .nullable(), users_to_organizations::role.nullable(), diff --git a/apps/api/turbo.json b/apps/api/turbo.json index 7751b1901..ff9c7a825 100644 --- a/apps/api/turbo.json +++ b/apps/api/turbo.json @@ -2,6 +2,10 @@ "$schema": "https://turbo.build/schema.json", "extends": ["//"], "tasks": { - "dev": {} + "dev": { + "dependsOn": ["@buster/database#start"], + "with": [], + "outputs": [] + } } } diff --git a/apps/server/src/api/v2/dictionaries/color-themes/config.ts b/apps/server/src/api/v2/dictionaries/color-themes/config.ts index 7b62edad1..da09522a1 100644 --- a/apps/server/src/api/v2/dictionaries/color-themes/config.ts +++ b/apps/server/src/api/v2/dictionaries/color-themes/config.ts @@ -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), + }) +); diff --git a/apps/server/src/api/v2/index.ts b/apps/server/src/api/v2/index.ts index d97462f09..10c139882 100644 --- a/apps/server/src/api/v2/index.ts +++ b/apps/server/src/api/v2/index.ts @@ -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; diff --git a/apps/server/src/api/v2/security/test-db-utils.ts b/apps/server/src/api/v2/security/test-db-utils.ts index 433bdce5b..d5f7d3767 100644 --- a/apps/server/src/api/v2/security/test-db-utils.ts +++ b/apps/server/src/api/v2/security/test-db-utils.ts @@ -63,7 +63,10 @@ export async function createTestOrganizationInDb( deletedAt: null, domain: null, paymentRequired: true, - organizationColorPalettes: [], + organizationColorPalettes: { + selectedId: null, + palettes: [], + }, ...orgData, }; diff --git a/apps/web/src/api/buster_rest/organizations/queryRequests.ts b/apps/web/src/api/buster_rest/organizations/queryRequests.ts index e39524f97..6c0b54fbd 100644 --- a/apps/web/src/api/buster_rest/organizations/queryRequests.ts +++ b/apps/web/src/api/buster_rest/organizations/queryRequests.ts @@ -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: () => { diff --git a/apps/web/src/app/app/(primary_layout)/(chat_experience)/chats/[chatId]/dashboards/[dashboardId]/layout.tsx b/apps/web/src/app/app/(primary_layout)/(chat_experience)/chats/[chatId]/dashboards/[dashboardId]/layout.tsx index 192f57e14..9c576ac31 100644 --- a/apps/web/src/app/app/(primary_layout)/(chat_experience)/chats/[chatId]/dashboards/[dashboardId]/layout.tsx +++ b/apps/web/src/app/app/(primary_layout)/(chat_experience)/chats/[chatId]/dashboards/[dashboardId]/layout.tsx @@ -3,7 +3,6 @@ import { DashboardLayout } from '@/layouts/DashboardLayout'; export default async function Layout({ children, params, - ...rest }: { children: React.ReactNode; params: Promise<{ dashboardId: string }>; diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/AddCustomThemeBase.tsx b/apps/web/src/components/features/colors/DefaultThemeSelector/AddCustomThemeBase.tsx index 7f9a37cd1..7534c4e3d 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/AddCustomThemeBase.tsx +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/AddCustomThemeBase.tsx @@ -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[] = 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}>
- + {iThemes.length > 0 && ( + + )}
@@ -87,25 +88,37 @@ const ThreeDotMenu: React.FC<{ theme: IColorTheme }> = React.memo(({ theme }) => await deleteCustomTheme(themeId); }); - return ; + const onUpdate = useMemoizedFn(async (theme: IColorTheme) => { + await modifyCustomTheme(theme.id, theme); + }); + + return ( + + ); }); ThreeDotMenu.displayName = 'ThreeDotMenu'; -const AddCustomThemeButton: React.FC<{}> = React.memo(({}) => { +const AddCustomThemeButton: React.FC = React.memo(({}) => { const { createCustomTheme } = useAddTheme(); + const buttonRef = useRef(null); + + const closePopover = useMemoizedFn(() => { + buttonRef.current?.click(); + }); const onSave = useMemoizedFn(async (theme: IColorTheme) => { await createCustomTheme(theme); + closePopover(); }); return ( } trigger="click" - className="p-0" + className="max-w-[320px] p-0" sideOffset={12}> - diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelector.tsx b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelector.tsx index fc80a801d..aef28ca18 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelector.tsx +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelector.tsx @@ -8,82 +8,92 @@ import { useColorThemes } from '@/api/buster_rest/dictionaries'; import { StatusCard } from '@/components/ui/card/StatusCard'; import { CircleSpinnerLoader } from '../../../ui/loaders'; -export const DefaultThemeSelector = React.memo(() => { - const { data: userData } = useGetMyUserInfo(); - const { data: themes, isFetched: isFetchedThemes, isError: isErrorThemes } = useColorThemes(); - const { mutateAsync: updateOrganization } = useUpdateOrganization(); +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; - await updateOrganization({ - organizationColorPalettes: { - selectedId: currentThemeId || theme.id, - palettes: [...organization.organizationColorPalettes.palettes, theme] - } + + const onCreateCustomTheme = useMemoizedFn(async (theme: IColorTheme) => { + if (!organization) return; + await updateOrganization({ + organizationColorPalettes: { + selectedId: theme.id, + palettes: [theme, ...organization.organizationColorPalettes.palettes] + } + }); }); - }); - const onDeleteCustomTheme = useMemoizedFn(async (themeId: string) => { - const currentThemeId = organization.organizationColorPalettes.selectedId; - const isSelectedTheme = currentThemeId === themeId; - const firstTheme = organization.organizationColorPalettes.palettes[0]; + const onDeleteCustomTheme = useMemoizedFn(async (themeId: string) => { + if (!organization) return; + const currentThemeId = organization.organizationColorPalettes.selectedId; + const isSelectedTheme = currentThemeId === themeId; - await updateOrganization({ - organizationColorPalettes: { - selectedId: isSelectedTheme ? firstTheme.id : currentThemeId, - palettes: organization.organizationColorPalettes.palettes.filter( - (theme) => theme.id !== themeId - ) - } + await updateOrganization({ + organizationColorPalettes: { + selectedId: isSelectedTheme ? null : currentThemeId, + palettes: organization.organizationColorPalettes.palettes.filter( + (theme) => theme.id !== themeId + ) + } + }); }); - }); - const onModifyCustomTheme = useMemoizedFn(async (themeId: string, theme: IColorTheme) => { - await updateOrganization({ - organizationColorPalettes: { - selectedId: organization.organizationColorPalettes.selectedId, - palettes: organization.organizationColorPalettes.palettes.map((t) => - t.id === themeId ? theme : t - ) - } + const onModifyCustomTheme = useMemoizedFn(async (themeId: string, theme: IColorTheme) => { + if (!organization) return; + + await updateOrganization({ + organizationColorPalettes: { + selectedId: organization.organizationColorPalettes.selectedId, + palettes: organization.organizationColorPalettes.palettes.map((t) => + t.id === themeId ? theme : t + ) + } + }); }); - }); - const onSelectTheme = useMemoizedFn((theme: IColorTheme) => { - updateOrganization({ - organizationColorPalettes: { - selectedId: theme.id, - palettes: organization.organizationColorPalettes.palettes - } + const onSelectTheme = useMemoizedFn((theme: IColorTheme) => { + if (!organization) return; + + updateOrganization({ + organizationColorPalettes: { + selectedId: theme.id, + palettes: organization.organizationColorPalettes.palettes + } + }); }); - }); - const organizationColorPalettes = organization?.organizationColorPalettes; - if (!isFetchedThemes) return ; + if (!organizationColorPalettes || !organization) return null; + + if (!isFetchedThemes) return ; + + if (isErrorThemes) + return ( + + ); - if (isErrorThemes) return ( - ); - - return ( - - ); -}); + } +); DefaultThemeSelector.displayName = 'DefaultThemeSelector'; diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.stories.tsx b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.stories.tsx index ac79c7f4f..a6b739170 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.stories.tsx +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.stories.tsx @@ -15,7 +15,6 @@ const meta: Meta = { onDeleteCustomTheme: fn(), onModifyCustomTheme: fn(), selectedThemeId: 'custom-sunset', - useDefaultThemes: true, customThemes: [ { name: 'Custom Sunset', diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.tsx b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.tsx index 4fab1a30e..0e36bf0a1 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.tsx +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/DefaultThemeSelectorBase.tsx @@ -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[]; @@ -13,43 +12,45 @@ export interface DefaultThemeSelectorProps { onDeleteCustomTheme: (themeId: string) => Promise; onModifyCustomTheme: (themeId: string, theme: IColorTheme) => Promise; 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[] = themes?.map((theme) => ({ ...theme, - selected: theme.id === selectedThemeId, - id: theme.name + selected: theme.id === selectedThemeId })); return ( -
+
+ +
-
-
- -
); } diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/DraggableColorPicker.tsx b/apps/web/src/components/features/colors/DefaultThemeSelector/DraggableColorPicker.tsx index 17c1bebad..137cbfc2b 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/DraggableColorPicker.tsx +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/DraggableColorPicker.tsx @@ -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 ( { + setColor(originalColor.current); + onPickerOpenChange(v); + }} popoverChildren={
+ )}
diff --git a/apps/web/src/components/features/colors/DefaultThemeSelector/index.ts b/apps/web/src/components/features/colors/DefaultThemeSelector/index.ts index 41b800007..48cda927f 100644 --- a/apps/web/src/components/features/colors/DefaultThemeSelector/index.ts +++ b/apps/web/src/components/features/colors/DefaultThemeSelector/index.ts @@ -1 +1 @@ -export * from './DefaultThemeSelectorBase'; +export * from './DefaultThemeSelector'; diff --git a/apps/web/src/components/features/colors/ThemeList/ThemeColorDots.tsx b/apps/web/src/components/features/colors/ThemeList/ThemeColorDots.tsx index 1e306273a..186771e24 100644 --- a/apps/web/src/components/features/colors/ThemeList/ThemeColorDots.tsx +++ b/apps/web/src/components/features/colors/ThemeList/ThemeColorDots.tsx @@ -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<{
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 }} diff --git a/apps/web/src/components/features/colors/ThemeList/ThemeList.tsx b/apps/web/src/components/features/colors/ThemeList/ThemeList.tsx index 26170941b..65b13c052 100644 --- a/apps/web/src/components/features/colors/ThemeList/ThemeList.tsx +++ b/apps/web/src/components/features/colors/ThemeList/ThemeList.tsx @@ -22,7 +22,7 @@ export const ThemeList: React.FC<{ )}> {themes.map((theme) => ( diff --git a/apps/web/src/components/features/colors/ThemeList/interfaces.ts b/apps/web/src/components/features/colors/ThemeList/interfaces.ts index e413e700e..be7def4a5 100644 --- a/apps/web/src/components/features/colors/ThemeList/interfaces.ts +++ b/apps/web/src/components/features/colors/ThemeList/interfaces.ts @@ -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; }; diff --git a/apps/web/src/components/features/settings/DefaultColorThemeCard.tsx b/apps/web/src/components/features/settings/DefaultColorThemeCard.tsx index 9f45ba9df..c193ae19c 100644 --- a/apps/web/src/components/features/settings/DefaultColorThemeCard.tsx +++ b/apps/web/src/components/features/settings/DefaultColorThemeCard.tsx @@ -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 ( +
Default color theme Default color theme that Buster will use when creating charts
-
PICKER
+
] } @@ -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 ( + + +
+ }> +
+
+ {hasDefaultColorTheme ? ( + + ) : ( + + No default color theme + + )} +
+ +
+ +
+
+ + ); +}); + +PickButton.displayName = 'PickButton'; diff --git a/apps/web/src/components/ui/popover/PopoverBase.tsx b/apps/web/src/components/ui/popover/PopoverBase.tsx index 1f3668d5e..bc6fdb8ec 100644 --- a/apps/web/src/components/ui/popover/PopoverBase.tsx +++ b/apps/web/src/components/ui/popover/PopoverBase.tsx @@ -12,6 +12,8 @@ const Popover = PopoverPrimitive.Root; interface PopoverProps extends React.ComponentPropsWithoutRef { trigger?: PopoverTriggerType; + children: React.ReactNode; + open?: boolean; } const PopoverRoot: React.FC = ({ children, trigger = 'click', ...props }) => { diff --git a/apps/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx b/apps/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx index 7efcdabcb..b550304a9 100644 --- a/apps/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx +++ b/apps/web/src/controllers/MetricController/MetricViewChart/MetricEditController/MetricStylingApp/MetricStylingApp.tsx @@ -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 (
diff --git a/packages/database/src/queries/organizations/update-organization.ts b/packages/database/src/queries/organizations/update-organization.ts index 7cfd1f7ae..5de0785d8 100644 --- a/packages/database/src/queries/organizations/update-organization.ts +++ b/packages/database/src/queries/organizations/update-organization.ts @@ -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; diff --git a/packages/server-shared/src/dictionary/color-themes.ts b/packages/server-shared/src/dictionary/color-themes.ts index 4d6660168..dd1c07af4 100644 --- a/packages/server-shared/src/dictionary/color-themes.ts +++ b/packages/server-shared/src/dictionary/color-themes.ts @@ -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[]; diff --git a/packages/server-shared/src/organization/organization.types.ts b/packages/server-shared/src/organization/organization.types.ts index 651765999..2d6a96d1f 100644 --- a/packages/server-shared/src/organization/organization.types.ts +++ b/packages/server-shared/src/organization/organization.types.ts @@ -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; export type OrganizationColorPalette = z.infer; +export type ColorPalette = z.infer; type _OrganizationEqualityCheck = Expect>; diff --git a/packages/server-shared/src/organization/requests.ts b/packages/server-shared/src/organization/requests.ts index d4985dae6..67e81e7ce 100644 --- a/packages/server-shared/src/organization/requests.ts +++ b/packages/server-shared/src/organization/requests.ts @@ -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;