initialize file views

This commit is contained in:
Nate Kelley 2025-04-16 09:38:02 -06:00
parent 4a832d984a
commit e6dfca0e25
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
9 changed files with 358 additions and 38 deletions

View File

@ -63,7 +63,7 @@ export const ReasoningMessagePills: React.FC<{
animate={pills.length > 0 ? 'visible' : 'hidden'}
className={'flex w-full flex-wrap gap-1.5 overflow-hidden'}>
{pills.map((pill) => (
<Link href={makeHref(pill)} key={pill.id} prefetch={false}>
<Link href={makeHref(pill)} key={pill.id} prefetch={true}>
<Pill useAnimation={useAnimation} {...pill} />
</Link>
))}
@ -88,10 +88,9 @@ const Pill: React.FC<{
onClick={() => !!id && !!type && onClick?.({ id, type })}
variants={pillVariants}
className={cn(
'text-text-secondary bg-item-active border-border hover:bg-item-hover-active h-[18px] min-h-[18px] rounded-sm border px-1 text-xs',
className,
'text-text-secondary bg-item-active border-border hover:bg-item-hover-active flex h-[18px] min-h-[18px] items-center justify-center rounded-sm border px-1 text-xs whitespace-nowrap transition-all',
!!onClick && 'cursor-pointer',
'flex items-center justify-center whitespace-nowrap'
className
)}>
{text}
</motion.div>

View File

@ -0,0 +1,41 @@
import { BusterRoutes } from '@/routes/busterRoutes';
import { getFileViewFromRoute } from './getFileViewFromRoute';
describe('getFileViewFromRoute', () => {
test('should return chart view for metric chart route', () => {
expect(getFileViewFromRoute(BusterRoutes.APP_METRIC_ID_CHART)).toBe('chart');
});
test('should return results view for metric results routes', () => {
expect(getFileViewFromRoute(BusterRoutes.APP_METRIC_ID_RESULTS)).toBe('results');
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_METRIC_ID_RESULTS)).toBe('results');
});
test('should return file view for file-related routes', () => {
expect(getFileViewFromRoute(BusterRoutes.APP_METRIC_ID_FILE)).toBe('file');
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_METRIC_ID_FILE)).toBe('file');
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_DASHBOARD_ID_FILE)).toBe('file');
expect(getFileViewFromRoute(BusterRoutes.APP_DASHBOARD_ID_FILE)).toBe('file');
});
test('should return dashboard view for dashboard routes', () => {
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_DASHBOARD_ID)).toBe('dashboard');
expect(getFileViewFromRoute(BusterRoutes.APP_DASHBOARD_ID)).toBe('dashboard');
});
test('should consistently return file view for chat metric routes', () => {
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_METRIC_ID)).toBe('file');
expect(getFileViewFromRoute(BusterRoutes.APP_CHAT_ID_METRIC_ID_FILE)).toBe('file');
});
test('should return undefined for routes not in the mapping', () => {
// @ts-expect-error Testing with an invalid route
expect(getFileViewFromRoute('INVALID_ROUTE')).toBeUndefined();
});
test('type safety - should accept only BusterRoutes enum values', () => {
// This test is mainly for TypeScript compilation - it should error if we try to pass invalid types
const validRoute = BusterRoutes.APP_DASHBOARD_ID;
expect(() => getFileViewFromRoute(validRoute)).not.toThrow();
});
});

View File

@ -1,4 +1,3 @@
export * from './ChatLayoutContext';
export * from './publicHelpers';
export * from './useLayoutConfig';
export * from './helpers';

View File

@ -1,8 +0,0 @@
import type { ThoughtFileType } from '@/api/asset_interfaces';
const OPENABLE_FILES = new Set<string>(['metric', 'dashboard', 'reasoning']);
export const isOpenableFile = (type: ThoughtFileType | null): boolean => {
if (!type) return false;
return OPENABLE_FILES.has(type);
};

View File

@ -0,0 +1,157 @@
import { renderHook } from '@testing-library/react';
import { useGetChatParams } from './useGetChatParams';
import * as navigation from 'next/navigation';
import * as appLayout from '@/context/BusterAppLayout';
// Mock the required hooks and modules
jest.mock('next/navigation', () => ({
useParams: jest.fn(),
useSearchParams: jest.fn(() => ({
get: jest.fn()
}))
}));
jest.mock('@/context/BusterAppLayout', () => ({
useAppLayoutContextSelector: jest.fn()
}));
describe('useGetChatParams', () => {
const mockUseParams = navigation.useParams as jest.Mock;
const mockUseSearchParams = navigation.useSearchParams as jest.Mock;
const mockUseAppLayoutContextSelector = appLayout.useAppLayoutContextSelector as jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockUseSearchParams.mockImplementation(() => ({
get: jest.fn().mockReturnValue(null)
}));
mockUseAppLayoutContextSelector.mockReturnValue('default-route');
});
test('returns undefined values when no params are provided', () => {
mockUseParams.mockReturnValue({});
const { result } = renderHook(() => useGetChatParams());
expect(result.current).toEqual({
isVersionHistoryMode: false,
chatId: undefined,
metricId: undefined,
dashboardId: undefined,
collectionId: undefined,
datasetId: undefined,
messageId: undefined,
metricVersionNumber: undefined,
dashboardVersionNumber: undefined,
currentRoute: 'default-route',
secondaryView: null
});
});
test('correctly processes chat and message IDs', () => {
mockUseParams.mockReturnValue({
chatId: 'chat-123',
messageId: 'msg-456'
});
const { result } = renderHook(() => useGetChatParams());
expect(result.current.chatId).toBe('chat-123');
expect(result.current.messageId).toBe('msg-456');
});
test('handles metric version number from path parameter', () => {
mockUseParams.mockReturnValue({
metricId: 'metric-123',
versionNumber: '42'
});
const { result } = renderHook(() => useGetChatParams());
expect(result.current.metricId).toBe('metric-123');
expect(result.current.metricVersionNumber).toBe(42);
});
test('handles metric version number from query parameter', () => {
mockUseParams.mockReturnValue({
metricId: 'metric-123'
});
mockUseSearchParams.mockImplementation(() => ({
get: (param: string) => (param === 'metric_version_number' ? '43' : null)
}));
const { result } = renderHook(() => useGetChatParams());
expect(result.current.metricVersionNumber).toBe(43);
});
test('handles dashboard version number from path parameter', () => {
mockUseParams.mockReturnValue({
dashboardId: 'dashboard-123',
versionNumber: '44'
});
const { result } = renderHook(() => useGetChatParams());
expect(result.current.dashboardId).toBe('dashboard-123');
expect(result.current.dashboardVersionNumber).toBe(44);
});
test('handles dashboard version number from query parameter', () => {
mockUseParams.mockReturnValue({
dashboardId: 'dashboard-123'
});
mockUseSearchParams.mockImplementation(() => ({
get: (param: string) => (param === 'dashboard_version_number' ? '45' : null)
}));
const { result } = renderHook(() => useGetChatParams());
expect(result.current.dashboardVersionNumber).toBe(45);
});
test('correctly identifies version history mode', () => {
mockUseSearchParams.mockImplementation(() => ({
get: (param: string) => (param === 'secondary_view' ? 'version-history' : null)
}));
const { result } = renderHook(() => useGetChatParams());
expect(result.current.isVersionHistoryMode).toBe(true);
});
test('handles collection and dataset IDs', () => {
mockUseParams.mockReturnValue({
collectionId: 'collection-123',
datasetId: 'dataset-456'
});
const { result } = renderHook(() => useGetChatParams());
expect(result.current.collectionId).toBe('collection-123');
expect(result.current.datasetId).toBe('dataset-456');
});
test('preserves current route from app layout context', () => {
mockUseAppLayoutContextSelector.mockReturnValue('custom-route');
const { result } = renderHook(() => useGetChatParams());
expect(result.current.currentRoute).toBe('custom-route');
});
test('returns consistent values on multiple renders without param changes', () => {
mockUseParams.mockReturnValue({
chatId: 'chat-123',
metricId: 'metric-123',
versionNumber: '46'
});
const { result, rerender } = renderHook(() => useGetChatParams());
const firstRender = result.current;
rerender();
expect(result.current).toEqual(firstRender);
});
});

View File

@ -1,7 +1,7 @@
'use client';
import { useAppLayoutContextSelector } from '@/context/BusterAppLayout';
import { useParams, useSearchParams, useSelectedLayoutSegments } from 'next/navigation';
import { useParams, useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import { FileViewSecondary } from './useLayoutConfig';
@ -24,7 +24,6 @@ export const useGetChatParams = () => {
messageId: string | undefined;
};
const searchParams = useSearchParams();
const segments = useSelectedLayoutSegments();
const queryMetricVersionNumber = searchParams.get('metric_version_number');
const queryDashboardVersionNumber = searchParams.get('dashboard_version_number');
const secondaryView = searchParams.get('secondary_view') as FileViewSecondary | undefined;

View File

@ -0,0 +1,144 @@
import { BusterRoutes } from '@/routes';
import { initializeFileViews } from './helpers';
import { FileViewSecondary } from './interfaces';
describe('initializeFileViews', () => {
it('should return empty object when no metricId or dashboardId is provided', () => {
const result = initializeFileViews({
metricId: undefined,
dashboardId: undefined,
currentRoute: BusterRoutes.APP_HOME,
secondaryView: undefined
});
expect(result).toEqual({});
});
it('should initialize metric view with chart as default when route does not map to a view', () => {
const metricId = 'metric123';
const result = initializeFileViews({
metricId,
dashboardId: undefined,
currentRoute: BusterRoutes.APP_HOME,
secondaryView: undefined
});
expect(result).toEqual({
[metricId]: {
selectedFileView: 'chart',
fileViewConfig: {
chart: {
secondaryView: undefined
}
}
}
});
});
it('should initialize metric view with specific view from route', () => {
const metricId = 'metric123';
const result = initializeFileViews({
metricId,
dashboardId: undefined,
currentRoute: BusterRoutes.APP_METRIC_ID_RESULTS,
secondaryView: undefined
});
expect(result).toEqual({
[metricId]: {
selectedFileView: 'results',
fileViewConfig: {
results: {
secondaryView: undefined
}
}
}
});
});
it('should initialize dashboard view with dashboard as default when route does not map to a view', () => {
const dashboardId = 'dashboard123';
const result = initializeFileViews({
metricId: undefined,
dashboardId,
currentRoute: BusterRoutes.APP_HOME,
secondaryView: undefined
});
expect(result).toEqual({
[dashboardId]: {
selectedFileView: 'dashboard',
fileViewConfig: {
dashboard: {
secondaryView: undefined
}
}
}
});
});
it('should initialize dashboard view with specific view from route', () => {
const dashboardId = 'dashboard123';
const result = initializeFileViews({
metricId: undefined,
dashboardId,
currentRoute: BusterRoutes.APP_DASHBOARD_ID_FILE,
secondaryView: undefined
});
expect(result).toEqual({
[dashboardId]: {
selectedFileView: 'file',
fileViewConfig: {
file: {
secondaryView: undefined
}
}
}
});
});
it('should include secondaryView in metric config when provided', () => {
const metricId = 'metric123';
const secondaryView: FileViewSecondary = 'chart-edit';
const result = initializeFileViews({
metricId,
dashboardId: undefined,
currentRoute: BusterRoutes.APP_METRIC_ID_CHART,
secondaryView
});
expect(result).toEqual({
[metricId]: {
selectedFileView: 'chart',
fileViewConfig: {
chart: {
secondaryView: 'chart-edit'
}
}
}
});
});
it('should include secondaryView in dashboard config when provided', () => {
const dashboardId = 'dashboard123';
const secondaryView: FileViewSecondary = 'version-history';
const result = initializeFileViews({
metricId: undefined,
dashboardId,
currentRoute: BusterRoutes.APP_DASHBOARD_ID,
secondaryView
});
expect(result).toEqual({
[dashboardId]: {
selectedFileView: 'dashboard',
fileViewConfig: {
dashboard: {
secondaryView: 'version-history'
}
}
}
});
});
});

View File

@ -7,8 +7,8 @@ import { useMemoizedFn, useUpdateEffect } from '@/hooks';
import { create } from 'mutative';
import { ChatLayoutView } from '../../interfaces';
import type { SelectedFile } from '../../interfaces';
import { timeout } from '@/lib';
import { BusterRoutes } from '@/routes';
import { timeout } from '@/lib/timeout';
import { BusterRoutes } from '@/routes/busterRoutes';
import { SelectedFileSecondaryRenderRecord } from '../../FileContainer/FileContainerSecondary';
import { ChatParams } from '../useGetChatParams';
import { initializeFileViews } from './helpers';
@ -194,24 +194,13 @@ export const useLayoutConfig = ({
});
}, [metricId, secondaryView, dashboardId, currentRoute]);
return useMemo(
() => ({
selectedLayout,
selectedFileView,
selectedFileViewSecondary,
selectedFileViewRenderSecondary,
onSetFileView,
closeSecondaryView,
onCollapseFileClick
}),
[
selectedLayout,
selectedFileView,
selectedFileViewSecondary,
selectedFileViewRenderSecondary,
onSetFileView,
closeSecondaryView,
onCollapseFileClick
]
);
return {
selectedLayout,
selectedFileView,
selectedFileViewSecondary,
selectedFileViewRenderSecondary,
onSetFileView,
closeSecondaryView,
onCollapseFileClick
};
};

View File

@ -77,7 +77,7 @@ export const FileContainer: React.FC<FileContainerProps> = ({ children }) => {
useUpdateLayoutEffect(() => {
setTimeout(() => {
//TODO revaluate this?
//TODO revaluate this? What is this for?
animateOpenSplitter(isOpenSecondary ? 'open' : 'closed');
}, 20);
}, [isOpenSecondary]);