mirror of https://github.com/buster-so/buster.git
Merge branch 'staging' into big-nate-bus-1939-quarters-are-still-not-being-displayed-correctly
This commit is contained in:
commit
be16261859
|
@ -127,6 +127,7 @@
|
|||
"@tiptap/react": "^3.5.0",
|
||||
"@tiptap/starter-kit": "^3.5.0",
|
||||
"@tiptap/suggestion": "^3.5.0",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@udecode/cn": "^49.0.15",
|
||||
"@uploadthing/react": "^7.3.3",
|
||||
"axios": "catalog:",
|
||||
|
@ -157,6 +158,7 @@
|
|||
"platejs": "49.2.21",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "^1.268.1",
|
||||
"qs": "^6.14.0",
|
||||
"react": "catalog:",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
|
|
|
@ -7,7 +7,6 @@ export type * from './datasources';
|
|||
export type * from './metric';
|
||||
export type * from './permission';
|
||||
export type * from './permission_groups';
|
||||
export type * from './search';
|
||||
export type * from './sql';
|
||||
export type * from './teams';
|
||||
export type * from './terms';
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export type * from './interfaces';
|
|
@ -1,10 +0,0 @@
|
|||
import type { ShareAssetType } from '@buster/server-shared/share';
|
||||
|
||||
export interface BusterSearchResult {
|
||||
id: string;
|
||||
highlights: string[];
|
||||
name: string;
|
||||
updated_at: string;
|
||||
type: ShareAssetType;
|
||||
score: number;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './queryRequests';
|
||||
export * from './queryRequestsV2';
|
||||
export * from './requests';
|
||||
export * from './requestsV2';
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
deleteChat,
|
||||
duplicateChat,
|
||||
getChat,
|
||||
getListChats,
|
||||
getListLogs,
|
||||
shareChat,
|
||||
unshareChat,
|
||||
|
@ -34,12 +33,13 @@ import {
|
|||
updateChatMessageFeedback,
|
||||
updateChatShare,
|
||||
} from './requests';
|
||||
import { getListChats } from './requestsV2';
|
||||
|
||||
export const useGetListChats = (
|
||||
filters?: Omit<Parameters<typeof getListChats>[0], 'page_token' | 'page_size'>
|
||||
filters?: Omit<Parameters<typeof getListChats>[0], 'page' | 'page_size'>
|
||||
) => {
|
||||
const filtersCompiled: Parameters<typeof getListChats>[0] = useMemo(
|
||||
() => ({ admin_view: false, page_token: 0, page_size: 5000, ...filters }),
|
||||
() => ({ admin_view: false, page: 1, page_size: 5000, ...filters }),
|
||||
[filters]
|
||||
);
|
||||
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
import type { ChatListItem } from '@buster/server-shared/chats';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mainApi } from '../instances';
|
||||
import { mainApiV2 } from '../instances';
|
||||
|
||||
// Mock the mainApi
|
||||
// Mock the mainApi and mainApiV2
|
||||
vi.mock('../instances', () => ({
|
||||
mainApi: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
mainApiV2: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Chat API Requests', () => {
|
||||
|
@ -36,17 +41,19 @@ describe('Chat API Requests', () => {
|
|||
];
|
||||
|
||||
// Setup mock response
|
||||
(mainApi.get as any).mockResolvedValueOnce({ data: mockChats });
|
||||
(mainApiV2.get as any).mockResolvedValueOnce({
|
||||
data: { data: mockChats }
|
||||
});
|
||||
|
||||
// Import the function we want to test
|
||||
const { getListChats } = await import('./requests');
|
||||
const { getListChats } = await import('./requestsV2');
|
||||
|
||||
// Execute the function
|
||||
const result = await getListChats();
|
||||
|
||||
// Verify the API was called with correct parameters
|
||||
expect(mainApi.get).toHaveBeenCalledWith('/chats', {
|
||||
params: { page_token: 0, page_size: 3500 },
|
||||
expect(mainApiV2.get).toHaveBeenCalledWith('/chats', {
|
||||
params: { page: 1, page_size: 3500 },
|
||||
});
|
||||
|
||||
// Verify the result matches the mock data
|
||||
|
|
|
@ -4,8 +4,6 @@ import type {
|
|||
DuplicateChatResponse,
|
||||
GetChatRequest,
|
||||
GetChatResponse,
|
||||
GetChatsListRequest,
|
||||
GetChatsListResponse,
|
||||
GetLogsListRequest,
|
||||
GetLogsListResponse,
|
||||
ShareChatResponse,
|
||||
|
@ -23,16 +21,6 @@ import { mainApi, mainApiV2 } from '../instances';
|
|||
|
||||
const CHATS_BASE = '/chats';
|
||||
|
||||
// Client-side fetch version
|
||||
export const getListChats = async (params?: GetChatsListRequest): Promise<GetChatsListResponse> => {
|
||||
const { page_token = 0, page_size = 3500 } = params || {};
|
||||
return mainApi
|
||||
.get<GetChatsListResponse>(`${CHATS_BASE}`, {
|
||||
params: { page_token, page_size },
|
||||
})
|
||||
.then((res) => res.data);
|
||||
};
|
||||
|
||||
export const getListLogs = async (params?: GetLogsListRequest): Promise<GetLogsListResponse> => {
|
||||
const { page_token = 0, page_size = 3500 } = params || {};
|
||||
return mainApi
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
import type { ChatCreateRequest, ChatWithMessages } from '@buster/server-shared/chats';
|
||||
import type {
|
||||
ChatCreateRequest,
|
||||
ChatWithMessages,
|
||||
GetChatsListResponseV2,
|
||||
GetChatsRequestV2,
|
||||
} from '@buster/server-shared/chats';
|
||||
import mainApi, { mainApiV2 } from '../instances';
|
||||
|
||||
export const getListChats = async (
|
||||
params?: GetChatsRequestV2
|
||||
): Promise<GetChatsListResponseV2['data']> => {
|
||||
const { page = 1, page_size = 3500 } = params || {};
|
||||
return mainApiV2
|
||||
.get<GetChatsListResponseV2>(`/chats`, {
|
||||
params: { page, page_size },
|
||||
})
|
||||
.then((res) => res.data.data);
|
||||
};
|
||||
|
||||
export const createNewChat = async (props: ChatCreateRequest) => {
|
||||
return mainApiV2.post<ChatWithMessages>('/chats', props).then((res) => res.data);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||
import { create } from 'mutative';
|
||||
import { dashboardQueryKeys } from '@/api/query_keys/dashboard';
|
||||
import { getOriginalDashboard } from '@/context/Dashboards/useOriginalDashboardStore';
|
||||
import type { dashboardsUpdateDashboard } from '../requests';
|
||||
import { useSaveDashboard } from './useSaveDashboard';
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,7 +32,7 @@ export const getDashboardById = async ({
|
|||
/** The version number of the dashboard */
|
||||
version_number?: number;
|
||||
}) => {
|
||||
return await mainApiV2
|
||||
return await mainApi
|
||||
.get<GetDashboardResponse>(`/dashboards/${id}`, {
|
||||
params: { password, version_number },
|
||||
})
|
||||
|
|
|
@ -83,7 +83,9 @@ export const useGetMetric = <TData = BusterMetric>(
|
|||
|
||||
const { isFetched: isFetchedInitial, isError: isErrorInitial } = useQuery({
|
||||
...metricsQueryKeys.metricsGetMetric(id || '', 'LATEST'),
|
||||
queryFn: () => getMetricQueryFn({ id, version: 'LATEST', queryClient, password }),
|
||||
queryFn: () => {
|
||||
return getMetricQueryFn({ id, version: 'LATEST', queryClient, password });
|
||||
},
|
||||
retry(_failureCount, error) {
|
||||
if (error?.message !== undefined && id) {
|
||||
setProtectedAssetPasswordError({
|
||||
|
@ -93,16 +95,19 @@ export const useGetMetric = <TData = BusterMetric>(
|
|||
}
|
||||
return false;
|
||||
},
|
||||
enabled: (params?.enabled ?? true) && !!id,
|
||||
select: undefined,
|
||||
...params,
|
||||
enabled: (params?.enabled ?? true) && !!id,
|
||||
});
|
||||
|
||||
return useQuery({
|
||||
...metricsQueryKeys.metricsGetMetric(id || '', selectedVersionNumber),
|
||||
enabled: !!id && !!latestVersionNumber && isFetchedInitial && !isErrorInitial,
|
||||
queryFn: () => getMetricQueryFn({ id, version: selectedVersionNumber, queryClient, password }),
|
||||
queryFn: () => {
|
||||
return getMetricQueryFn({ id, version: selectedVersionNumber, queryClient, password });
|
||||
},
|
||||
...params,
|
||||
select: params?.select,
|
||||
enabled: !!id && !!latestVersionNumber && isFetchedInitial && !isErrorInitial,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -31,9 +31,9 @@ export const getMetric = async ({
|
|||
id,
|
||||
...params
|
||||
}: GetMetricParams & GetMetricQuery): Promise<GetMetricResponse> => {
|
||||
return mainApiV2.get<GetMetricResponse>(`/metric_files/${id}`, { params }).then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
return mainApiV2
|
||||
.get<GetMetricResponse>(`/metric_files/${id}`, { params })
|
||||
.then(({ data }) => data);
|
||||
};
|
||||
|
||||
export const getMetricData = async ({
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import type { SearchTextResponse } from '@buster/server-shared/search';
|
||||
import { keepPreviousData, type UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
import type { RustApiError } from '@/api/errors';
|
||||
import { searchQueryKeys } from '@/api/query_keys/search';
|
||||
import { search } from './requests';
|
||||
|
||||
export const useSearch = (
|
||||
export const useSearch = <T = SearchTextResponse>(
|
||||
params: Parameters<typeof search>[0],
|
||||
options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof search>>>, 'queryKey' | 'queryFn'>
|
||||
options?: Omit<UseQueryOptions<SearchTextResponse, RustApiError, T>, 'queryKey' | 'queryFn'>,
|
||||
postQueryOptions?: {
|
||||
doNotUnwrapData?: boolean;
|
||||
}
|
||||
) => {
|
||||
return useQuery({
|
||||
const { doNotUnwrapData = false } = postQueryOptions || {};
|
||||
return useQuery<SearchTextResponse, RustApiError, T>({
|
||||
...searchQueryKeys.getSearchResult(params),
|
||||
queryFn: () => search(params),
|
||||
placeholderData: keepPreviousData,
|
||||
select: options?.select,
|
||||
...options,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import type { AssetType } from '@buster/server-shared/assets';
|
||||
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
|
||||
import { mainApi } from '../instances';
|
||||
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared/search';
|
||||
import qs from 'qs';
|
||||
import { mainApiV2 } from '../instances';
|
||||
|
||||
export const search = async (params: {
|
||||
query: string;
|
||||
asset_types: Extract<AssetType, 'dashboard_file' | 'metric_file' | 'collection'>[];
|
||||
num_results?: number;
|
||||
}) => {
|
||||
return mainApi.post<BusterSearchResult[]>('/search', params).then((res) => res.data);
|
||||
export const search = async (params: SearchTextRequest) => {
|
||||
return mainApiV2.get<SearchTextResponse>('/search', { params }).then((res) => res.data);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { isServer } from '@tanstack/react-query';
|
||||
import type { AxiosRequestHeaders } from 'axios';
|
||||
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';
|
||||
import qs from 'qs';
|
||||
import { getSupabaseSession } from '@/integrations/supabase/getSupabaseUserClient';
|
||||
import { Route as AuthRoute } from '@/routes/auth.login';
|
||||
import { BASE_URL_V2 } from './config';
|
||||
|
@ -15,6 +16,9 @@ export const createAxiosInstance = (baseURL = BASE_URL_V2) => {
|
|||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
paramsSerializer: (params) => {
|
||||
return qs.stringify(params, { arrayFormat: 'repeat' }); //💰
|
||||
},
|
||||
});
|
||||
|
||||
// Response interceptor with retry logic for auth errors
|
||||
|
|
|
@ -2,7 +2,8 @@ import type { ChatListItem } from '@buster/server-shared/chats';
|
|||
import { queryOptions } from '@tanstack/react-query';
|
||||
import type { BusterChatMessage, IBusterChat } from '@/api/asset_interfaces/chat';
|
||||
import type { BusterMetricData } from '@/api/asset_interfaces/metric/metricDataInterfaces';
|
||||
import type { getListChats, getListLogs } from '@/api/buster_rest/chats/requests';
|
||||
import type { getListLogs } from '@/api/buster_rest/chats/requests';
|
||||
import type { getListChats } from '@/api/buster_rest/chats/requestsV2';
|
||||
|
||||
const chatsGetChat = (chatId: string) =>
|
||||
queryOptions<IBusterChat>({
|
||||
|
@ -25,9 +26,7 @@ const chatsMessagesFetchingData = (messageId: string) =>
|
|||
enabled: !!messageId,
|
||||
});
|
||||
|
||||
const chatsGetList = (
|
||||
filters?: Omit<Parameters<typeof getListChats>[0], 'page_token' | 'page_size'>
|
||||
) =>
|
||||
const chatsGetList = (filters?: Omit<Parameters<typeof getListChats>[0], 'page' | 'page_size'>) =>
|
||||
queryOptions<ChatListItem[]>({
|
||||
queryKey: [
|
||||
'chats',
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared';
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
|
||||
import type { search } from '../buster_rest/search';
|
||||
|
||||
export const getSearchResult = (params: Parameters<typeof search>[0]) =>
|
||||
queryOptions<BusterSearchResult[]>({
|
||||
export const getSearchResult = (params: SearchTextRequest) =>
|
||||
queryOptions<SearchTextResponse>({
|
||||
queryKey: ['search', 'results', params] as const,
|
||||
staleTime: 1000 * 15, // 15 seconds,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { SearchTextResponse } from '@buster/server-shared/search';
|
||||
import type { ShareAssetType } from '@buster/server-shared/share';
|
||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
||||
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
|
||||
import {
|
||||
useAddAndRemoveAssetsFromCollection,
|
||||
useGetCollection,
|
||||
|
@ -33,8 +33,9 @@ export const AddToCollectionModal: React.FC<{
|
|||
|
||||
const { data: searchResults } = useSearch({
|
||||
query: debouncedSearchTerm,
|
||||
asset_types: ['metric_file', 'dashboard_file'],
|
||||
num_results: 100,
|
||||
assetTypes: ['metric_file', 'dashboard_file', 'report_file'],
|
||||
page_size: 50,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const [selectedAssets, setSelectedAssets] = useState<SelectedAsset[]>([]);
|
||||
|
@ -42,13 +43,13 @@ export const AddToCollectionModal: React.FC<{
|
|||
return selectedAssets.map((asset) => asset.id);
|
||||
}, [selectedAssets]);
|
||||
|
||||
const columns = useMemo<InputSelectModalProps<BusterSearchResult>['columns']>(
|
||||
const columns = useMemo<InputSelectModalProps<SearchTextResponse['data'][number]>['columns']>(
|
||||
() => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
render: (name, data) => {
|
||||
const Icon = assetTypeToIcon(data.type) || ASSET_ICONS.metrics;
|
||||
dataIndex: 'title',
|
||||
render: (name, record) => {
|
||||
const Icon = assetTypeToIcon(record.assetType) || ASSET_ICONS.metrics;
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-icon-color">
|
||||
|
@ -61,7 +62,7 @@ export const AddToCollectionModal: React.FC<{
|
|||
},
|
||||
{
|
||||
title: 'Updated',
|
||||
dataIndex: 'updated_at',
|
||||
dataIndex: 'updatedAt',
|
||||
width: 140,
|
||||
render: (value: string) => {
|
||||
return formatDate({
|
||||
|
@ -74,10 +75,10 @@ export const AddToCollectionModal: React.FC<{
|
|||
[]
|
||||
);
|
||||
|
||||
const rows: BusterListRowItem<BusterSearchResult>[] = useMemo(() => {
|
||||
const rows: BusterListRowItem<SearchTextResponse['data'][number]>[] = useMemo(() => {
|
||||
return (
|
||||
searchResults?.map((asset) => ({
|
||||
id: asset.id,
|
||||
searchResults?.data?.map((asset) => ({
|
||||
id: asset.assetId,
|
||||
data: asset,
|
||||
})) || []
|
||||
);
|
||||
|
@ -86,7 +87,7 @@ export const AddToCollectionModal: React.FC<{
|
|||
const createKeySearchResultMap = useMemoizedFn(() => {
|
||||
const map = new Map<string, SelectedAsset>();
|
||||
rows.forEach((asset) => {
|
||||
if (asset.data?.type) map.set(asset.id, { type: asset.data?.type, id: asset.id });
|
||||
if (asset.data?.assetType) map.set(asset.id, { type: asset.data?.assetType, id: asset.id });
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import type { BusterSearchResult } from '@/api/asset_interfaces/search';
|
||||
import type { SearchTextRequest, SearchTextResponse } from '@buster/server-shared/search';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearch } from '@/api/buster_rest/search';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import {
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import { assetTypeToIcon } from '../icons/assetIcons';
|
||||
|
||||
export const AddMetricModal: React.FC<{
|
||||
open: boolean;
|
||||
|
@ -46,21 +47,34 @@ export const AddMetricModal: React.FC<{
|
|||
const { data: searchResults } = useSearch(
|
||||
{
|
||||
query: debouncedSearchTerm,
|
||||
asset_types: ['metric_file'],
|
||||
num_results: 100,
|
||||
},
|
||||
{ enabled: true }
|
||||
assetTypes: ['metric_file'],
|
||||
page_size: 25,
|
||||
page: 1,
|
||||
} satisfies SearchTextRequest,
|
||||
{ enabled: true, select: useCallback((data: SearchTextResponse) => data.data, []) }
|
||||
);
|
||||
|
||||
const columns = useMemo<InputSelectModalProps<BusterSearchResult>['columns']>(
|
||||
const columns = useMemo<InputSelectModalProps<SearchTextResponse['data'][number]>['columns']>(
|
||||
() => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
title: 'Title',
|
||||
dataIndex: 'title',
|
||||
render: (value, record) => {
|
||||
const Icon = assetTypeToIcon(record.assetType);
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-icon-color">
|
||||
<Icon />
|
||||
</span>
|
||||
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: this endpoint is sanitized */}
|
||||
<span dangerouslySetInnerHTML={{ __html: value }}></span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Updated',
|
||||
dataIndex: 'updated_at',
|
||||
title: 'Updated at',
|
||||
dataIndex: 'updatedAt',
|
||||
width: 140,
|
||||
render: (value) => {
|
||||
return formatDate({
|
||||
|
@ -76,8 +90,8 @@ export const AddMetricModal: React.FC<{
|
|||
const rows = useMemo(() => {
|
||||
return (
|
||||
searchResults?.map((result) => ({
|
||||
id: result.id,
|
||||
dataTestId: `item-${result.id}`,
|
||||
id: result.assetId,
|
||||
dataTestId: `item-${result.assetId}`,
|
||||
data: result,
|
||||
})) || []
|
||||
);
|
||||
|
@ -100,7 +114,7 @@ export const AddMetricModal: React.FC<{
|
|||
const item = rows.find((row) => row.id === id);
|
||||
return {
|
||||
id: id,
|
||||
name: item?.data?.name || id,
|
||||
name: item?.data?.title || id,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
SquareChart,
|
||||
Table,
|
||||
} from '@/components/ui/icons';
|
||||
import type { iconProps } from '@/components/ui/icons/NucleoIconOutlined/iconProps';
|
||||
import SquareChartPlus from '@/components/ui/icons/NucleoIconOutlined/square-chart-plus';
|
||||
|
||||
export const ASSET_ICONS = {
|
||||
|
|
|
@ -16,7 +16,6 @@ export type MetricChartCardProps = {
|
|||
metricId: string;
|
||||
versionNumber: number | undefined;
|
||||
readOnly?: boolean;
|
||||
className?: string;
|
||||
attributes?: DraggableAttributes;
|
||||
listeners?: DraggableSyntheticListeners;
|
||||
headerSecondaryContent?: React.ReactNode;
|
||||
|
@ -25,7 +24,7 @@ export type MetricChartCardProps = {
|
|||
renderChartContent?: boolean; // we do this to avoid expensive rendering if off screen
|
||||
disableTooltip?: boolean;
|
||||
cacheDataId?: string;
|
||||
};
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const stableMetricSelect = ({
|
||||
chart_config,
|
||||
|
@ -62,6 +61,7 @@ export const MetricChartCard = React.memo(
|
|||
renderChartContent = true,
|
||||
disableTooltip,
|
||||
cacheDataId,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
|
@ -102,6 +102,7 @@ export const MetricChartCard = React.memo(
|
|||
errorData={errorData}
|
||||
isTable={isTable}
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<MetricViewChartHeader
|
||||
name={name}
|
||||
|
@ -144,13 +145,12 @@ type MetricViewChartCardContainerProps = {
|
|||
hasData: boolean;
|
||||
errorData: boolean;
|
||||
isTable: boolean;
|
||||
className?: string;
|
||||
};
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const MetricViewChartCardContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
MetricViewChartCardContainerProps
|
||||
>(({ children, loadingData, hasData, errorData, isTable, className }, ref) => {
|
||||
>(({ children, loadingData, hasData, errorData, isTable, className, ...divProps }, ref) => {
|
||||
const cardClass = React.useMemo(() => {
|
||||
if (loadingData || errorData || !hasData) return 'h-full max-h-[600px]';
|
||||
if (isTable) return 'h-full';
|
||||
|
@ -161,6 +161,7 @@ const MetricViewChartCardContainer = React.forwardRef<
|
|||
<MetricViewChartProvider>
|
||||
<div
|
||||
ref={ref}
|
||||
{...divProps}
|
||||
className={cn(
|
||||
'bg-background flex flex-col overflow-hidden rounded border shadow',
|
||||
cardClass,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { MetricChartCard } from '@/components/features/metrics/MetricChartCard';
|
|||
import { ReportMetricThreeDotMenu } from '@/components/features/metrics/ReportMetricItem';
|
||||
import { useGetReportParams } from '@/context/Reports/useGetReportParams';
|
||||
import { useInViewport } from '@/hooks/useInViewport';
|
||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||
|
||||
export const MetricContent = React.memo(
|
||||
({
|
||||
|
@ -30,6 +31,10 @@ export const MetricContent = React.memo(
|
|||
}
|
||||
const renderChart = inViewport || isExportMode || hasBeenInViewport.current;
|
||||
|
||||
const handleCopy = useMemoizedFn((e: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
return (
|
||||
<MetricChartCard
|
||||
ref={ref}
|
||||
|
@ -50,6 +55,7 @@ export const MetricContent = React.memo(
|
|||
/>
|
||||
)
|
||||
}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,3 +53,8 @@ p {
|
|||
th {
|
||||
@apply text-base font-normal;
|
||||
}
|
||||
|
||||
strong,
|
||||
b {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
|
|
@ -239,9 +239,9 @@ You operate in a loop to complete tasks:
|
|||
- The majority of explanation should go in the report, only use the done-tool to summarize the report and list any potential issues
|
||||
- Explain major assumptions that could impact the results
|
||||
- Explain the meaning of calculations that are made in the report or metric
|
||||
- You should create a metric for all calculations referenced in the report.
|
||||
- Create a metric object for each key calculation, but combine related metrics into a single visualization when they share the same categorical dimension (use grouped bars or a combo chart with dual axes as needed). Creating multiple metrics does not justify multiple charts in the same section.
|
||||
- Avoid creating large comprehensive tables that combine multiple metrics; instead, build individual metrics and use comprehensive views only to highlight specific interesting items (e.g., a table showing all data for a few interesting data points).
|
||||
- You should create a visualization for all calculations referenced in the report.
|
||||
- Create a metric object (a visualization) for each key calculation, but combine related metrics into a single visualization when they share the same categorical dimension (use grouped bars or a combo chart with dual axes as needed). Creating multiple metrics does not justify multiple charts in the same section.
|
||||
- Avoid creating large comprehensive tables that combine multiple metrics; instead, build individual visualizations and use comprehensive views only to highlight specific interesting items (e.g., a table showing all data for a few interesting data points).
|
||||
- You should always have a methodolgy section that explains the data, calculations, decisions, and assumptions made for each metric or definition. You can have a more technical tone in this final section.
|
||||
- Style Guidelines:
|
||||
- Use **bold** for key words, phrases, data points, or ideas that should be highlighted.
|
||||
|
@ -399,7 +399,7 @@ You operate in a loop to complete tasks:
|
|||
- Proportions: Prefer bar charts, but pie or donut charts can be used
|
||||
- Relationships between two variables: Use scatter plots to visualize correlations or patterns
|
||||
- Multiple data series over time: Use combo charts with multiple y-axes to display different metrics or categories
|
||||
- Use combo charts only when they clarify relationships between two or more related metrics, especially when the metrics have different scales or units (e.g., "revenue in dollars vs. conversion rate in %").
|
||||
- Use combo charts only when they clarify relationships between two or more related metrics, especially when the metrics have different scales or units (e.g., "revenue in dollars vs. conversion rate in %").
|
||||
- Preferred use case: bars for absolute values (totals, counts, amounts) and a line for trends, ratios, or rates.
|
||||
- Avoid combo charts when all metrics share the same unit/scale or when the relationship between metrics is weak or redundant—use a simpler chart instead.
|
||||
- Limit to two series/axes whenever possible; adding more can make the chart confusing or visually cluttered.
|
||||
|
@ -407,6 +407,13 @@ You operate in a loop to complete tasks:
|
|||
- Assign the primary metric (larger values or main focus) to the left y-axis.
|
||||
- Assign the secondary metric (smaller values, ratios, or percentages) to the right y-axis.
|
||||
- Ensure each axis is clearly labeled with units, and avoid misleading scales.
|
||||
- **Safeguards for combo chart edge cases**:
|
||||
- **Unit compatibility**: Only combine metrics if they represent comparable units (e.g., counts vs. counts, dollars vs. dollars, percentages vs. percentages). Do not combine metrics with fundamentally different units (e.g., dollars vs clicks) on the same axis.
|
||||
- **Scale alignment**: Before combining, compare the ranges of the metrics. If one metric is multiple orders of magnitude larger than the other (e.g., 5k-10k vs. 20M-40M), separate them into different charts or different axes.
|
||||
- **Ratios and rates exception**: If one metric is a ratio or percentage (e.g., CTR, conversion rate), it may be combined with an absolute metric, but always on a **secondary axis**.
|
||||
- Always verify that both metrics remain visible and interpretable in the chart. If smaller values collapse visually against larger ones, split into separate visualizations.
|
||||
- Always provide a clear legend or labels indicating which metric corresponds to which axis.
|
||||
- Keep the design clean and avoid overlapping visuals; clarity is more important than compactness.
|
||||
- For ambiguous requests (e.g., "Show me our revenue"), default to line charts to show trends over time. This provides both the trend and the latest value, covering multiple possibilities
|
||||
- Use number cards for displaying single values or key metrics (e.g., "Total Revenue: $1000")
|
||||
- For requests identifying a single item (e.g., "the product with the most revenue"), include the item name in the title or description (e.g., "Revenue of Top Product: Product X - $500")
|
||||
|
|
|
@ -737,8 +737,13 @@ If all true → proceed to submit prep for Asset Creation with `submitThoughts`.
|
|||
- Assign the primary metric (larger values or main focus) to the left y-axis.
|
||||
- Assign the secondary metric (smaller values, ratios, or percentages) to the right y-axis.
|
||||
- Ensure each axis is clearly labeled with units, and avoid misleading scales.
|
||||
- **Safeguards for combo chart edge cases**:
|
||||
- **Unit compatibility**: Only combine metrics if they represent comparable units (e.g., counts vs. counts, dollars vs. dollars, percentages vs. percentages). Do not combine metrics with fundamentally different units (e.g., dollars vs clicks) on the same axis.
|
||||
- **Scale alignment**: Before combining, compare the ranges of the metrics. If one metric is multiple orders of magnitude larger than the other (e.g., 5k-10k vs. 20M-40M), separate them into different charts or different axes.
|
||||
- **Ratios and rates exception**: If one metric is a ratio or percentage (e.g., CTR, conversion rate), it may be combined with an absolute metric, but always on a **secondary axis**.
|
||||
- Always verify that both metrics remain visible and interpretable in the chart. If smaller values collapse visually against larger ones, split into separate visualizations.
|
||||
- Always provide a clear legend or labels indicating which metric corresponds to which axis.
|
||||
- Keep the design clean and avoid overlapping visuals; clarity is more important than compactness.
|
||||
- Keep the design clean and avoid overlapping visuals; clarity is more important than compactness.
|
||||
- Use tables only when:
|
||||
- Specifically requested by the user.
|
||||
- Displaying detailed lists with many items.
|
||||
|
|
|
@ -591,8 +591,13 @@ When in doubt, be more thorough rather than less. Reports are the default becaus
|
|||
- Assign the primary metric (larger values or main focus) to the left y-axis.
|
||||
- Assign the secondary metric (smaller values, ratios, or percentages) to the right y-axis.
|
||||
- Ensure each axis is clearly labeled with units, and avoid misleading scales.
|
||||
- **Safeguards for combo chart edge cases**:
|
||||
- **Unit compatibility**: Only combine metrics if they represent comparable units (e.g., counts vs. counts, dollars vs. dollars, percentages vs. percentages). Do not combine metrics with fundamentally different units (e.g., dollars vs clicks) on the same axis.
|
||||
- **Scale alignment**: Before combining, compare the ranges of the metrics. If one metric is multiple orders of magnitude larger than the other (e.g., 5k-10k vs. 20M-40M), separate them into different charts or different axes.
|
||||
- **Ratios and rates exception**: If one metric is a ratio or percentage (e.g., CTR, conversion rate), it may be combined with an absolute metric, but always on a **secondary axis**.
|
||||
- Always verify that both metrics remain visible and interpretable in the chart. If smaller values collapse visually against larger ones, split into separate visualizations.
|
||||
- Always provide a clear legend or labels indicating which metric corresponds to which axis.
|
||||
- Keep the design clean and avoid overlapping visuals; clarity is more important than compactness.
|
||||
- Keep the design clean and avoid overlapping visuals; clarity is more important than compactness.
|
||||
- Use tables only when:
|
||||
- Specifically requested by the user.
|
||||
- Displaying detailed lists with many items.
|
||||
|
|
|
@ -156,15 +156,51 @@ properties:
|
|||
type: string
|
||||
description: |
|
||||
Human-readable time period covered by the SQL query, similar to a filter in a BI tool.
|
||||
RULE: Must accurately reflect the date/time filter used in the `sql` field. Do not misrepresent the time range.
|
||||
Examples:
|
||||
|
||||
## CRITICAL RULES
|
||||
- Each metric/visualization should have its own unique timeframe string based on the date filters used in the metric's `sql` field
|
||||
- The timeframe must be derived directly and only from that metric’s `sql` field.
|
||||
- Never generalize across metrics or apply a “one size fits all” timeframe for multiple charts. If charts are on the same report or dashboard but utilize different date filters, you must display the correct timeFrame for each metric/visualization.
|
||||
- Must accurately reflect both the date filters and the grouping granularity used in the SQL.
|
||||
|
||||
## Allowed Formats
|
||||
- Fixed Dates: "January 1, 2020 - December 31, 2020", "2024", "Q2 2024", "June 1, 2025"
|
||||
- Relative Dates: "Today", "Yesterday", "Last 7 days", "Last 30 days", "Last Quarter", "Last 12 Months", "Year to Date", "All time"
|
||||
- Comparisons: Use the format "Comparison: [Period 1] vs [Period 2]". Examples:
|
||||
- "Comparison: Last 30 days vs Previous 30 days"
|
||||
- "Comparison: June 1, 2025 - June 30, 2025 vs July 1, 2025 - July 31, 2025"
|
||||
RULE: Use full month names for dates, e.g., "January", not "Jan".
|
||||
RULE: Follow general quoting rules. CANNOT contain ':'.
|
||||
- Relative Dates: "Today", "Yesterday", "Last 7 days", "Last 30 days", "Last Quarter",
|
||||
"Last 12 Months", "Year to Date", "Quarter to Date", "Month to Date", "Week to Date", "All time"
|
||||
- Comparisons: "Comparison - [Period 1] vs [Period 2]"
|
||||
Examples:
|
||||
- "Comparison - Last 30 days vs Previous 30 days"
|
||||
- "Comparison - Last 12 Months vs Previous 12 Months"
|
||||
- "Comparison - June 1, 2025 - June 30, 2025 vs July 1, 2025 - July 31, 2025"
|
||||
- Fiscal Periods: "FY2024", "FQ3 2024"
|
||||
- Forecast/Future: "Next 12 Months", "Next Quarter"
|
||||
|
||||
## Edge Case Rules
|
||||
- **Incomplete start date only** (`WHERE date >= '2023-01-01'`) → "Since January 1, 2023"
|
||||
- **Incomplete end date only** (`WHERE date <= '2024-06-30'`) → "Through June 30, 2024"
|
||||
- **Current partial period** (`WHERE YEAR(order_date) = 2024` during 2024) → "Year to Date"
|
||||
- **Custom rolling windows** (`WHERE date >= CURRENT_DATE - INTERVAL '90 days'`) → "Last 90 days"
|
||||
- **Future projections** (`WHERE date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '12 months'`) → "Next 12 Months"
|
||||
- **Short fixed spans** (3 months of data: Jan–Mar 2025) → "January 2025 - March 2025"
|
||||
- **Fiscal calendar filters** (`WHERE fiscal_quarter = 'FQ3 2024'`) → "FQ3 2024"
|
||||
- **Rolling to-date filters** (`DATE_TRUNC('quarter', CURRENT_DATE)`) → "Quarter to Date"
|
||||
- **Relative offsets** (`WHERE date BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '30 days'`) → "30–60 Days Ago"
|
||||
|
||||
## Formatting Rules
|
||||
- Always use full month names ("January", not "Jan").
|
||||
- Do not include ":" inside the timeframe string.
|
||||
- Timeframe string must match SQL exactly (filters and grouping).
|
||||
- Never reuse the same timeframe across unrelated metrics unless the SQL is identical.
|
||||
|
||||
## Examples
|
||||
- Query grouped by month, filtered last 12 months → "Last 12 Months"
|
||||
- Query grouped by quarter, filtered current year → "Q1 2025 - Q3 2025"
|
||||
- Query with WHERE order_date BETWEEN '2021-01-01' AND '2021-12-31' → "January 1, 2021 - December 31, 2021"
|
||||
- Query with no date filter → "All time"
|
||||
- Comparison of rolling periods → "Comparison - Last 30 days vs Previous 30 days"
|
||||
- Open-ended filter (`>= 2023-01-01`) → "Since January 1, 2023"
|
||||
- Forecasting 1 year ahead → "Next 12 Months"
|
||||
- Fiscal quarter filter → "FQ3 2024"
|
||||
|
||||
# SQL QUERY
|
||||
sql:
|
||||
|
|
|
@ -9,12 +9,8 @@ import {
|
|||
users,
|
||||
usersToOrganizations,
|
||||
} from '../../schema';
|
||||
import type { ChatListItem } from '../../schema-types';
|
||||
import {
|
||||
type PaginatedResponse,
|
||||
PaginationInputSchema,
|
||||
createPaginatedResponse,
|
||||
} from '../shared-types';
|
||||
import type { ChatListItem, PaginatedResponse } from '../../schema-types';
|
||||
import { PaginationInputSchema, createPaginatedResponse } from '../../schema-types';
|
||||
|
||||
export const ListChatsRequestSchema = z
|
||||
.object({
|
||||
|
|
|
@ -2,9 +2,12 @@ import { type InferSelectModel, and, count, desc, eq, gte, isNull, like, lte } f
|
|||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { reportFiles, users } from '../../schema';
|
||||
import {
|
||||
type PaginatedResponse,
|
||||
createPaginatedResponse,
|
||||
withPagination,
|
||||
} from '../../schema-types';
|
||||
import { getUserOrganizationId } from '../organizations';
|
||||
import { type PaginatedResponse, createPaginatedResponse } from '../shared-types';
|
||||
import { withPagination } from '../shared-types/with-pagination';
|
||||
|
||||
export const GetReportsListInputSchema = z.object({
|
||||
userId: z.string().uuid('User ID must be a valid UUID'),
|
||||
|
|
|
@ -2,9 +2,12 @@ import { type SQL, and, count, desc, eq, exists, isNull, ne, or, sql } from 'dri
|
|||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { assetPermissions, reportFiles, teamsToUsers, users } from '../../schema';
|
||||
import {
|
||||
type PaginatedResponse,
|
||||
createPaginatedResponse,
|
||||
withPagination,
|
||||
} from '../../schema-types';
|
||||
import { getUserOrganizationId } from '../organizations';
|
||||
import { type PaginatedResponse, createPaginatedResponse } from '../shared-types';
|
||||
import { withPagination } from '../shared-types/with-pagination';
|
||||
|
||||
export const GetReportsWithPermissionsInputSchema = z.object({
|
||||
userId: z.string().uuid('User ID must be a valid UUID'),
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
type PaginatedResponse,
|
||||
PaginationInputSchema,
|
||||
createPaginatedResponse,
|
||||
} from '../shared-types';
|
||||
} from '../../schema-types';
|
||||
import { createPermissionedAssetsSubquery } from './access-control-helpers';
|
||||
|
||||
import { AssetTypeSchema } from '../../schema-types/asset';
|
||||
|
@ -100,6 +100,7 @@ export async function searchText(input: SearchTextInput): Promise<SearchTextResp
|
|||
assetType: assetSearchV2.assetType,
|
||||
title: assetSearchV2.title,
|
||||
additionalText: assetSearchV2.additionalText,
|
||||
updatedAt: assetSearchV2.updatedAt,
|
||||
})
|
||||
.from(assetSearchV2)
|
||||
.innerJoin(
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
// Export pagination types and utilities
|
||||
export * from './pagination.types';
|
||||
export * from './with-pagination';
|
|
@ -1,30 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Pagination input schema for validation
|
||||
export const PaginationInputSchema = z.object({
|
||||
page: z.number().min(1).optional().default(1),
|
||||
page_size: z.number().min(1).max(1000).optional().default(250),
|
||||
});
|
||||
|
||||
export type PaginationInput = z.infer<typeof PaginationInputSchema>;
|
||||
|
||||
// Pagination metadata that's returned with results
|
||||
export interface PaginationMetadata {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
// Generic paginated response type
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: PaginationMetadata;
|
||||
}
|
||||
|
||||
// Type helper for creating paginated API responses
|
||||
export type WithPagination<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {
|
||||
pagination: PaginationMetadata;
|
||||
};
|
|
@ -8,9 +8,12 @@ import {
|
|||
usersToOrganizations,
|
||||
} from '../../schema';
|
||||
import { UserOrganizationRoleSchema, UserOrganizationStatusSchema } from '../../schema-types';
|
||||
import {
|
||||
type PaginatedResponse,
|
||||
createPaginatedResponse,
|
||||
withPagination,
|
||||
} from '../../schema-types';
|
||||
import { getUserOrganizationId } from '../organizations/organizations';
|
||||
import { type PaginatedResponse, createPaginatedResponse } from '../shared-types';
|
||||
import { withPagination } from '../shared-types/with-pagination';
|
||||
|
||||
// Type-safe schema types
|
||||
type User = InferSelectModel<typeof users>;
|
||||
|
|
|
@ -37,3 +37,5 @@ export * from './github';
|
|||
export * from './search';
|
||||
|
||||
export * from './chat';
|
||||
|
||||
export * from './pagination';
|
||||
|
|
|
@ -1,6 +1,36 @@
|
|||
import type { SQL } from 'drizzle-orm';
|
||||
import type { PgColumn, PgSelect } from 'drizzle-orm/pg-core';
|
||||
import type { PaginatedResponse, PaginationMetadata } from './pagination.types';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Pagination input schema for validation
|
||||
export const PaginationInputSchema = z.object({
|
||||
page: z.coerce.number().min(1).optional().default(1),
|
||||
page_size: z.coerce.number().min(1).max(5000).optional().default(250),
|
||||
});
|
||||
|
||||
export type PaginationInput = z.infer<typeof PaginationInputSchema>;
|
||||
|
||||
export const PaginationSchema = z.object({
|
||||
page: z.number(),
|
||||
page_size: z.number(),
|
||||
total: z.number(),
|
||||
total_pages: z.number(),
|
||||
});
|
||||
|
||||
export type PaginationMetadata = z.infer<typeof PaginationSchema>;
|
||||
|
||||
// Generic paginated response type
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
pagination: PaginationMetadata;
|
||||
}
|
||||
|
||||
// Type helper for creating paginated API responses
|
||||
export type WithPagination<T> = {
|
||||
[K in keyof T]: T[K];
|
||||
} & {
|
||||
pagination: PaginationMetadata;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds pagination to a Drizzle query using the dynamic query builder pattern
|
|
@ -1,8 +1,10 @@
|
|||
import z from 'zod';
|
||||
import { AssetTypeSchema } from './asset';
|
||||
|
||||
export const TextSearchResultSchema = z.object({
|
||||
assetId: z.string().uuid(),
|
||||
assetType: z.string(),
|
||||
assetType: AssetTypeSchema,
|
||||
title: z.string(),
|
||||
additionalText: z.string().nullable(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
|
|
@ -99,6 +99,10 @@
|
|||
"./healthcheck": {
|
||||
"types": "./dist/healthcheck/index.d.ts",
|
||||
"default": "./dist/healthcheck/index.js"
|
||||
},
|
||||
"./search": {
|
||||
"types": "./dist/search/index.d.ts",
|
||||
"default": "./dist/search/index.js"
|
||||
}
|
||||
},
|
||||
"module": "src/index.ts",
|
||||
|
|
|
@ -28,7 +28,8 @@ export const SearchTextRequestSchema = z
|
|||
}
|
||||
return Boolean(val);
|
||||
}, z.boolean())
|
||||
.default(false),
|
||||
.default(false)
|
||||
.optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
startDate: z.string().datetime().optional(),
|
||||
})
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import {
|
||||
PaginationInputSchema,
|
||||
type PaginationMetadata,
|
||||
PaginationSchema,
|
||||
} from '@buster/database/schema-types';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PaginationSchema = z.object({
|
||||
page: z.number(),
|
||||
page_size: z.number(),
|
||||
total: z.number(),
|
||||
total_pages: z.number(),
|
||||
});
|
||||
export { PaginationSchema, type PaginationMetadata } from '@buster/database/schema-types';
|
||||
|
||||
export type Pagination = z.infer<typeof PaginationSchema>;
|
||||
export type Pagination = PaginationMetadata;
|
||||
|
||||
export const PaginatedResponseSchema = <T>(schema: z.ZodType<T>) =>
|
||||
z.object({
|
||||
|
@ -17,7 +17,4 @@ export const PaginatedResponseSchema = <T>(schema: z.ZodType<T>) =>
|
|||
|
||||
export type PaginatedResponse<T> = z.infer<ReturnType<typeof PaginatedResponseSchema<T>>>;
|
||||
|
||||
export const PaginatedRequestSchema = z.object({
|
||||
page: z.coerce.number().min(1).default(1),
|
||||
page_size: z.coerce.number().min(1).max(5000).default(250),
|
||||
});
|
||||
export const PaginatedRequestSchema = PaginationInputSchema;
|
||||
|
|
|
@ -703,6 +703,9 @@ importers:
|
|||
'@tiptap/suggestion':
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0(@tiptap/core@3.5.0(@tiptap/pm@3.5.0))(@tiptap/pm@3.5.0)
|
||||
'@types/qs':
|
||||
specifier: ^6.14.0
|
||||
version: 6.14.0
|
||||
'@udecode/cn':
|
||||
specifier: ^49.0.15
|
||||
version: 49.0.15(@types/react@19.1.13)(class-variance-authority@0.7.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(tailwind-merge@3.3.1)
|
||||
|
@ -793,6 +796,9 @@ importers:
|
|||
posthog-js:
|
||||
specifier: ^1.268.1
|
||||
version: 1.268.1
|
||||
qs:
|
||||
specifier: ^6.14.0
|
||||
version: 6.14.0
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.1.1
|
||||
|
@ -6356,6 +6362,9 @@ packages:
|
|||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/qs@6.14.0':
|
||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||
|
||||
'@types/react-dom@19.1.9':
|
||||
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
|
||||
peerDependencies:
|
||||
|
@ -19323,6 +19332,8 @@ snapshots:
|
|||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/qs@6.14.0': {}
|
||||
|
||||
'@types/react-dom@19.1.9(@types/react@19.1.13)':
|
||||
dependencies:
|
||||
'@types/react': 19.1.13
|
||||
|
@ -19591,7 +19602,7 @@ snapshots:
|
|||
sirv: 3.0.1
|
||||
tinyglobby: 0.2.14
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(sass@1.93.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
|
||||
vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.1)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
|
|
Loading…
Reference in New Issue