move currency around to dictionary

This commit is contained in:
Nate Kelley 2025-07-18 12:34:39 -06:00
parent ec2a3f6238
commit 95e0c8d7b5
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
20 changed files with 62 additions and 273 deletions

View File

@ -1,4 +1,4 @@
import type { Currency } from '@buster/server-shared/currency';
import type { Currency } from '@buster/server-shared/dictionary';
const codeToFlag = {
AED: '🇦🇪',

View File

@ -1,4 +1,4 @@
import type { CurrencyResponse } from '@buster/server-shared/currency';
import type { CurrencyResponse } from '@buster/server-shared/dictionary';
import { Hono } from 'hono';
import { CURRENCIES_MAP } from './config';

View File

@ -1 +0,0 @@
export * from './queryRequests';

View File

@ -1,26 +0,0 @@
import { queryKeys } from '@/api/query_keys';
import { QueryClient, usePrefetchQuery, useQuery } from '@tanstack/react-query';
import { mainApiV2 } from '../instances';
import { Currency } from '@buster/server-shared/currency';
export const useGetCurrencies = () => {
return useQuery({
...queryKeys.getCurrencies,
queryFn: async () => {
return mainApiV2.get<Currency[]>('/dictionaries/currency').then(async (res) => res.data);
}
});
};
export const prefetchGetCurrencies = async (queryClientProp?: QueryClient) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
...queryKeys.getCurrencies,
queryFn: async () => {
return mainApiV2.get<Currency[]>('/dictionaries/currency').then(async (res) => res.data);
}
});
return queryClient;
};

View File

@ -1,11 +1,10 @@
import { useQuery, QueryClient } from '@tanstack/react-query';
import { getColorThemes } from './requests';
import { dictionariesQueryKeys } from '../../query_keys/dictionaries';
import { getColorThemes, getCurrencies } from './requests';
import { dictionariesQueryKeys } from '@/api/query_keys/dictionaries';
export const useColorDictionaryThemes = () => {
return useQuery({
...dictionariesQueryKeys.colorThemes,
initialData: [],
queryFn: getColorThemes
});
};
@ -20,3 +19,21 @@ export const prefetchColorThemes = async (queryClientProp?: QueryClient) => {
return queryClient;
};
export const useGetCurrencies = () => {
return useQuery({
...dictionariesQueryKeys.getCurrencies,
queryFn: getCurrencies
});
};
export const prefetchGetCurrencies = async (queryClientProp?: QueryClient) => {
const queryClient = queryClientProp || new QueryClient();
await queryClient.prefetchQuery({
...dictionariesQueryKeys.getCurrencies,
queryFn: getCurrencies
});
return queryClient;
};

View File

@ -1,8 +1,13 @@
import type { ColorThemeDictionariesResponse } from '@buster/server-shared/dictionary';
import { mainApiV2 } from '../instances';
import type { CurrencyResponse } from '@buster/server-shared/dictionary';
export const getColorThemes = async () => {
return await mainApiV2
.get<ColorThemeDictionariesResponse>('/dictionaries/color-themes')
.then((res) => res.data);
};
export const getCurrencies = async () => {
return await mainApiV2.get<CurrencyResponse>('/dictionaries/currency').then((res) => res.data);
};

View File

@ -1,13 +0,0 @@
import { queryOptions } from '@tanstack/react-query';
import { CurrencyResponse } from '@buster/server-shared/currency';
export const getCurrencies = queryOptions<CurrencyResponse>({
queryKey: ['nextjs', 'list', 'currencies'],
initialData: [],
initialDataUpdatedAt: 0,
staleTime: 1000 * 60 * 60 * 24 * 7 //7 days
});
export const currencyQueryKeys = {
getCurrencies
};

View File

@ -1,11 +1,22 @@
import type { CurrencyResponse } from '@buster/server-shared/dictionary';
import type { ColorThemeDictionariesResponse } from '@buster/server-shared/dictionary';
import { queryOptions } from '@tanstack/react-query';
export const colorThemes = queryOptions<ColorThemeDictionariesResponse>({
queryKey: ['color-themes', 'list'],
staleTime: 60 * 1000 * 60 * 24 * 3 // 3 days
queryKey: ['dictionaries', 'color-themes', 'list'] as const,
initialData: [],
initialDataUpdatedAt: 0,
staleTime: 1000 * 60 * 60 * 24 * 7 // 7 days
});
export const getCurrencies = queryOptions<CurrencyResponse>({
queryKey: ['dictionaries', 'currencies', 'list'] as const,
initialData: [],
initialDataUpdatedAt: 0,
staleTime: 1000 * 60 * 60 * 24 * 7 // 7 days
});
export const dictionariesQueryKeys = {
colorThemes
colorThemes,
getCurrencies
};

View File

@ -5,7 +5,6 @@ import { datasetGroupQueryKeys } from './dataset_groups';
import { datasetQueryKeys } from './datasets';
import { datasourceQueryKeys } from './datasources';
import { metricsQueryKeys } from './metric';
import { currencyQueryKeys } from './currency';
import { permissionGroupQueryKeys } from './permission_groups';
import { searchQueryKeys } from './search';
import { termsQueryKeys } from './terms';
@ -26,7 +25,6 @@ export const queryKeys = {
...datasourceQueryKeys,
...datasetGroupQueryKeys,
...permissionGroupQueryKeys,
...currencyQueryKeys,
...securityQueryKeys,
...slackQueryKeys,
...dictionariesQueryKeys

View File

@ -1,7 +1,5 @@
import React from 'react';
import { DefaultThemeSelectorBase } from './DefaultThemeSelectorBase';
import { useGetMyUserInfo } from '@/api/buster_rest/users/queryRequests';
import { useColorDictionaryThemes } from '@/api/buster_rest/dictionaries';
import { StatusCard } from '@/components/ui/card/StatusCard';
import { CircleSpinnerLoader } from '../../../ui/loaders';
import { useThemeOperations } from '@/context-hooks/useThemeOperations';
@ -9,11 +7,8 @@ import { useGetPalettes } from '@/context-hooks/usePalettes';
export const DefaultThemeSelector = React.memo(
({ className, themeListClassName }: { className?: string; themeListClassName?: string }) => {
const { data: userData } = useGetMyUserInfo();
const {
isErrorDictionaryPalettes,
isFetchedDictionaryPalettes,
organizationPalettes,
dictionaryPalettes,
selectedPaletteId
@ -22,7 +17,12 @@ export const DefaultThemeSelector = React.memo(
const { onCreateCustomTheme, onDeleteCustomTheme, onModifyCustomTheme, onSelectTheme } =
useThemeOperations();
if (!isFetchedDictionaryPalettes) return <CircleSpinnerLoader />;
if (dictionaryPalettes.length === 0 && !isErrorDictionaryPalettes)
return (
<div className="flex h-24 w-full min-w-24 items-center justify-center">
<CircleSpinnerLoader size={24} />
</div>
);
if (isErrorDictionaryPalettes)
return (

View File

@ -40,10 +40,6 @@ const PickButton = React.memo(() => {
const hasDefaultPalette = !!defaultPalette;
useMount(() => {
prefetchColorThemes();
});
return (
<Popover
className="p-0"

View File

@ -3,6 +3,7 @@ import type { ColorPalette } from '@buster/server-shared/organization';
import { useMemo } from 'react';
import { useColorDictionaryThemes } from '../api/buster_rest/dictionaries';
import { DEFAULT_CHART_THEME } from '@buster/server-shared/metrics';
import { useGetCurrencies } from '../api/buster_rest/dictionaries';
const useGetOrganizationPalettes = () => {
const { data: userData } = useGetMyUserInfo();
@ -27,11 +28,11 @@ const useGetOrganizationPalettes = () => {
export const useGetPalettes = () => {
const { organizationPalettes, selectedPaletteId } = useGetOrganizationPalettes();
const {
data: dictionaryPalettes,
isFetched: isFetchedDictionaryPalettes,
isError: isErrorDictionaryPalettes
} = useColorDictionaryThemes();
const { data: dictionaryPalettes, isError: isErrorDictionaryPalettes } =
useColorDictionaryThemes();
const { data: currencies } = useGetCurrencies();
console.log(currencies, dictionaryPalettes);
return useMemo(() => {
const allPalettes = [...dictionaryPalettes, ...organizationPalettes];
@ -43,10 +44,10 @@ export const useGetPalettes = () => {
dictionaryPalettes,
selectedPaletteId,
defaultPalette,
isFetchedDictionaryPalettes,
isErrorDictionaryPalettes
};
}, [dictionaryPalettes, organizationPalettes, selectedPaletteId, isFetchedDictionaryPalettes]);
}, [dictionaryPalettes, organizationPalettes, selectedPaletteId]);
};
export const useSelectedColorPalette = (colors: string[] | undefined | null): string[] => {

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import type { ColumnLabelFormat } from '@buster/server-shared/metrics';
import { useGetCurrencies } from '@/api/buster_rest/currency';
import { useGetCurrencies } from '@/api/buster_rest/dictionaries';
import { Select, type SelectItem } from '@/components/ui/select';
import { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks';

View File

@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import type { ChartConfigProps } from '@buster/server-shared/metrics';
import { type ChartEncodes, type ColumnSettings } from '@buster/server-shared/metrics';
import type { ColumnLabelFormat } from '@buster/server-shared/metrics';
import { prefetchGetCurrencies } from '@/api/buster_rest/currency';
import { prefetchGetCurrencies } from '@/api/buster_rest/dictionaries';
import { ErrorBoundary } from '@/components/ui/error';
import { Text } from '@/components/ui/typography';
import { useUpdateMetricChart } from '@/context/Metrics';

View File

@ -36,10 +36,6 @@
"types": "./dist/dashboards/index.d.ts",
"default": "./dist/dashboards/index.js"
},
"./currency": {
"types": "./dist/currency/index.d.ts",
"default": "./dist/currency/index.js"
},
"./slack": {
"types": "./dist/slack/index.d.ts",
"default": "./dist/slack/index.js"

View File

@ -1,192 +0,0 @@
import { describe, expect, it } from 'vitest';
import { CurrencySchema } from './currency.types';
describe('CurrencySchema', () => {
it('should parse valid currency object', () => {
const validCurrency = {
code: 'USD',
description: 'United States Dollar',
flag: '🇺🇸',
};
const result = CurrencySchema.safeParse(validCurrency);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('USD');
expect(result.data.description).toBe('United States Dollar');
expect(result.data.flag).toBe('🇺🇸');
}
});
it('should require all fields', () => {
const incompleteObjects = [
{
// missing code
description: 'Euro',
flag: '🇪🇺',
},
{
code: 'EUR',
// missing description
flag: '🇪🇺',
},
{
code: 'EUR',
description: 'Euro',
// missing flag
},
];
for (const obj of incompleteObjects) {
const result = CurrencySchema.safeParse(obj);
expect(result.success).toBe(false);
}
});
it('should validate that all fields are strings', () => {
const invalidTypes = [
{
code: 123, // should be string
description: 'Invalid Code',
flag: '🇺🇸',
},
{
code: 'USD',
description: true, // should be string
flag: '🇺🇸',
},
{
code: 'USD',
description: 'United States Dollar',
flag: null, // should be string
},
];
for (const obj of invalidTypes) {
const result = CurrencySchema.safeParse(obj);
expect(result.success).toBe(false);
}
});
it('should handle various currency examples', () => {
const currencies = [
{
code: 'EUR',
description: 'Euro',
flag: '🇪🇺',
},
{
code: 'GBP',
description: 'British Pound Sterling',
flag: '🇬🇧',
},
{
code: 'JPY',
description: 'Japanese Yen',
flag: '🇯🇵',
},
{
code: 'CAD',
description: 'Canadian Dollar',
flag: '🇨🇦',
},
{
code: 'AUD',
description: 'Australian Dollar',
flag: '🇦🇺',
},
];
for (const currency of currencies) {
const result = CurrencySchema.safeParse(currency);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe(currency.code);
expect(result.data.description).toBe(currency.description);
expect(result.data.flag).toBe(currency.flag);
}
}
});
it('should handle empty strings', () => {
const currencyWithEmptyStrings = {
code: '',
description: '',
flag: '',
};
const result = CurrencySchema.safeParse(currencyWithEmptyStrings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('');
expect(result.data.description).toBe('');
expect(result.data.flag).toBe('');
}
});
it('should handle long strings', () => {
const currencyWithLongStrings = {
code: 'VERYLONGCURRENCYCODE',
description:
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema',
flag: '🏳️‍🌈🏳️‍⚧️🇺🇳', // Multiple flag emojis
};
const result = CurrencySchema.safeParse(currencyWithLongStrings);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('VERYLONGCURRENCYCODE');
expect(result.data.description).toBe(
'This is a very long description for a currency that might not exist in real life but should still be valid according to our schema'
);
expect(result.data.flag).toBe('🏳️‍🌈🏳️‍⚧️🇺🇳');
}
});
it('should handle special characters and unicode', () => {
const currencyWithSpecialChars = {
code: 'BTC-₿',
description: 'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}',
flag: '₿',
};
const result = CurrencySchema.safeParse(currencyWithSpecialChars);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.code).toBe('BTC-₿');
expect(result.data.description).toBe(
'Bitcoin (₿) - Digital Currency with special chars: @#$%^&*()[]{}'
);
expect(result.data.flag).toBe('₿');
}
});
it('should reject additional properties', () => {
const currencyWithExtraProps = {
code: 'USD',
description: 'United States Dollar',
flag: '🇺🇸',
extraProperty: 'This should not be allowed',
anotherExtra: 123,
};
const result = CurrencySchema.safeParse(currencyWithExtraProps);
// Zod by default allows additional properties unless .strict() is used
// Since the schema doesn't use .strict(), extra properties are allowed but ignored
expect(result.success).toBe(true);
if (result.success) {
// Extra properties should not be included in the result
expect('extraProperty' in result.data).toBe(false);
expect('anotherExtra' in result.data).toBe(false);
expect(result.data.code).toBe('USD');
expect(result.data.description).toBe('United States Dollar');
expect(result.data.flag).toBe('🇺🇸');
}
});
});

View File

@ -1,2 +0,0 @@
export * from './currency.types';
export * from './responses';

View File

@ -1,6 +0,0 @@
import { z } from 'zod';
import { CurrencySchema } from './currency.types';
export const CurrencyResponseSchema = z.array(CurrencySchema);
export type CurrencyResponse = z.infer<typeof CurrencyResponseSchema>;

View File

@ -7,3 +7,7 @@ export const CurrencySchema = z.object({
});
export type Currency = z.infer<typeof CurrencySchema>;
export const CurrencyResponseSchema = z.array(CurrencySchema);
export type CurrencyResponse = z.infer<typeof CurrencyResponseSchema>;

View File

@ -1 +1,2 @@
export * from './color-themes';
export * from './currency';