mirror of https://github.com/buster-so/buster.git
type safe routing update
This commit is contained in:
parent
776be1a68a
commit
3073cdb9e6
|
@ -1,176 +0,0 @@
|
|||
# Asset Route Builder
|
||||
|
||||
A type-safe route builder for TanStack Start/Router that dynamically generates routes based on asset types and parameters.
|
||||
|
||||
## Overview
|
||||
|
||||
The Asset Route Builder provides a clean, type-safe way to generate routes for different asset types (chats, metrics, dashboards, reports) with various parameter combinations. It ensures that the generated routes match the available route files in your TanStack Start application.
|
||||
|
||||
## Features
|
||||
|
||||
- **Type Safety**: All routes are validated against `FileRouteTypes['id']` from the generated route tree
|
||||
- **Fluent API**: Clean builder pattern for constructing routes
|
||||
- **Dynamic Route Generation**: Automatically determines the correct route based on provided parameters
|
||||
- **Parameter Extraction**: Provides params object for navigation
|
||||
|
||||
## Available Routes
|
||||
|
||||
The system supports the following route patterns:
|
||||
|
||||
### Single Asset Routes
|
||||
- `/app/chats/$chatId`
|
||||
- `/app/dashboards/$dashboardId`
|
||||
- `/app/metrics/$metricId`
|
||||
- `/app/reports/$reportId`
|
||||
|
||||
### Chat Context Routes
|
||||
- `/app/chats/$chatId/dashboard/$dashboardId`
|
||||
- `/app/chats/$chatId/metrics/$metricId`
|
||||
- `/app/chats/$chatId/report/$reportId`
|
||||
|
||||
### Nested Context Routes
|
||||
- `/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId`
|
||||
- `/app/chats/$chatId/report/$reportId/metrics/$metricId`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage with `assetParamsToRoute`
|
||||
|
||||
```typescript
|
||||
import { assetParamsToRoute } from '@/lib/assets/assetParamsToRoute';
|
||||
|
||||
// Navigate to a single asset
|
||||
const chatRoute = assetParamsToRoute({
|
||||
assetType: 'chat',
|
||||
assetId: 'chat-123',
|
||||
});
|
||||
// Returns: '/app/chats/$chatId'
|
||||
|
||||
// Navigate to a metric within a chat
|
||||
const metricInChatRoute = assetParamsToRoute({
|
||||
assetType: 'metric',
|
||||
assetId: 'metric-456',
|
||||
chatId: 'chat-123',
|
||||
});
|
||||
// Returns: '/app/chats/$chatId/metrics/$metricId'
|
||||
|
||||
// Navigate to a metric within a dashboard in a chat
|
||||
const complexRoute = assetParamsToRoute({
|
||||
assetType: 'metric',
|
||||
assetId: 'metric-789',
|
||||
chatId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
});
|
||||
// Returns: '/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId'
|
||||
```
|
||||
|
||||
### Using the RouteBuilder
|
||||
|
||||
```typescript
|
||||
import { createRouteBuilder } from '@/lib/assets/assetParamsToRoute';
|
||||
|
||||
// Build routes step by step
|
||||
const builder = createRouteBuilder()
|
||||
.withChat('chat-123')
|
||||
.withDashboard('dash-456')
|
||||
.withMetric('metric-789');
|
||||
|
||||
const route = builder.build();
|
||||
// Returns: '/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId'
|
||||
|
||||
const params = builder.getParams();
|
||||
// Returns: { chatId: 'chat-123', dashboardId: 'dash-456', metricId: 'metric-789' }
|
||||
```
|
||||
|
||||
### Integration with TanStack Router
|
||||
|
||||
```typescript
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { createAssetNavigation } from '@/lib/assets/assetParamsToRoute.example';
|
||||
|
||||
function MyComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleNavigation = () => {
|
||||
const { route, params } = createAssetNavigation({
|
||||
assetType: 'dashboard',
|
||||
assetId: 'dash-123',
|
||||
chatId: 'chat-456',
|
||||
});
|
||||
|
||||
// Use with your navigation API
|
||||
navigate({ to: route, params });
|
||||
};
|
||||
|
||||
return <button onClick={handleNavigation}>Go to Dashboard</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `assetParamsToRoute(params: AssetParamsToRoute): RouteFilePaths`
|
||||
|
||||
Main function to convert asset parameters to a route path.
|
||||
|
||||
#### Parameters
|
||||
|
||||
- `params`: An object containing:
|
||||
- `assetType`: The type of asset ('chat' | 'metric' | 'dashboard' | 'report')
|
||||
- `assetId`: The ID of the main asset
|
||||
- Additional optional context parameters (chatId, metricId, dashboardId, reportId)
|
||||
|
||||
#### Returns
|
||||
|
||||
A type-safe route path matching `FileRouteTypes['id']`.
|
||||
|
||||
### `createRouteBuilder(): RouteBuilder`
|
||||
|
||||
Creates a new RouteBuilder instance for fluent route construction.
|
||||
|
||||
#### RouteBuilder Methods
|
||||
|
||||
- `withChat(chatId: string)`: Add a chat ID to the route
|
||||
- `withMetric(metricId: string)`: Add a metric ID to the route
|
||||
- `withDashboard(dashboardId: string)`: Add a dashboard ID to the route
|
||||
- `withReport(reportId: string)`: Add a report ID to the route
|
||||
- `build()`: Build the final route path
|
||||
- `getParams()`: Get the params object for navigation
|
||||
|
||||
## Type Definitions
|
||||
|
||||
```typescript
|
||||
type ChatParamsToRoute = {
|
||||
assetType: 'chat';
|
||||
assetId: string;
|
||||
metricId?: string;
|
||||
dashboardId?: string;
|
||||
reportId?: string;
|
||||
};
|
||||
|
||||
type MetricParamsToRoute = {
|
||||
assetType: 'metric';
|
||||
assetId: string;
|
||||
dashboardId?: string;
|
||||
reportId?: string;
|
||||
chatId?: string;
|
||||
};
|
||||
|
||||
type DashboardParamsToRoute = {
|
||||
assetType: 'dashboard';
|
||||
assetId: string;
|
||||
metricId?: string;
|
||||
reportId?: string;
|
||||
chatId?: string;
|
||||
};
|
||||
|
||||
type ReportParamsToRoute = {
|
||||
assetType: 'report';
|
||||
assetId: string;
|
||||
metricId?: string;
|
||||
chatId?: string;
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See `assetParamsToRoute.example.tsx` for comprehensive usage examples and `assetParamsToRoute.test.ts` for test cases.
|
|
@ -1,7 +1,7 @@
|
|||
import type { AssetType } from '@buster/server-shared/assets';
|
||||
import type { AnyRouter, NavigateOptions, RegisteredRouter } from '@tanstack/react-router';
|
||||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
import type { BusterNavigateOptions } from './typeSafeNavigation';
|
||||
import type { BusterNavigateOptions } from '../tss-routes';
|
||||
|
||||
type RouteFilePaths = FileRouteTypes['id'];
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import { routeToHref } from '../tss-routes';
|
||||
import {
|
||||
type AssetParamsToRoute,
|
||||
assetParamsToRoute,
|
||||
assetParamsToRoutePath,
|
||||
} from './assetParamsToRoute';
|
||||
import { navigationOptionsToHref, toHref } from './typeSafeNavigation';
|
||||
|
||||
// Example React component showing usage
|
||||
export function AssetNavigationExample() {
|
||||
|
@ -59,11 +59,11 @@ export function AssetNavigationExample() {
|
|||
});
|
||||
|
||||
// Convert to actual URL
|
||||
const reportHref = navigationOptionsToHref(reportNavOptions);
|
||||
const reportHref = routeToHref(reportNavOptions);
|
||||
console.log(reportHref); // '/app/chats/chat-123/report/report-999'
|
||||
|
||||
// Example 6: Using with native anchor tags
|
||||
const dashboardHref = toHref(dashboardNavOptions);
|
||||
const dashboardHref = routeToHref(dashboardNavOptions);
|
||||
// dashboardHref is: '/app/chats/chat-123/dashboard/dash-789/metrics/metric-456'
|
||||
|
||||
return (
|
||||
|
@ -97,7 +97,7 @@ export function AssetNavigationExample() {
|
|||
// Example function that accepts AssetParamsToRoute
|
||||
export function createAssetLink(params: AssetParamsToRoute) {
|
||||
const navOptions = assetParamsToRoute(params);
|
||||
const href = toHref(navOptions);
|
||||
const href = routeToHref(navOptions);
|
||||
|
||||
// You can use either Link component or native anchor
|
||||
return (
|
||||
|
@ -128,8 +128,8 @@ export function generateShareableLinks() {
|
|||
|
||||
// Convert to absolute URLs for sharing
|
||||
const baseUrl = window.location.origin;
|
||||
const chatHref = baseUrl + toHref(chatOptions);
|
||||
const metricHref = baseUrl + toHref(metricOptions);
|
||||
const chatHref = baseUrl + routeToHref(chatOptions);
|
||||
const metricHref = baseUrl + routeToHref(metricOptions);
|
||||
|
||||
console.log('Share these links:');
|
||||
console.log('Chat:', chatHref); // https://example.com/app/chats/chat-123
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
import type { NavigateOptions, RegisteredRouter } from '@tanstack/react-router';
|
||||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
|
||||
/**
|
||||
* Type representing navigation options that can be passed to testNavigate
|
||||
* This is useful for creating type-safe route objects
|
||||
*
|
||||
* IMPORTANT: Always provide type parameters when using this type!
|
||||
*
|
||||
* ❌ Bad: BusterNavigateOptions (no type safety)
|
||||
* ✅ Good: BusterNavigateOptions<'/', '/app/chats/$chatId'>
|
||||
*
|
||||
* Without type parameters, TypeScript won't enforce required params!
|
||||
*/
|
||||
export type BusterNavigateOptions<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
> = NavigateOptions<RegisteredRouter, TFrom, TTo, TMaskFrom, TMaskTo>;
|
||||
|
||||
/**
|
||||
* Type-safe navigate function for testing that matches the behavior of useNavigate()
|
||||
* This function provides the same type safety as the regular navigate hook
|
||||
* by leveraging the RegisteredRouter type which contains all route definitions
|
||||
*/
|
||||
export function acceptsTypeSafeNavigateOptions<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
>(options: BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo>): void {
|
||||
// In a test environment, you might want to just log or store the navigation
|
||||
// For actual implementation, you could:
|
||||
// 1. Store the navigation in a test spy
|
||||
// 2. Update window.location in a test environment
|
||||
// 3. Use a mock router's navigate method
|
||||
|
||||
// For now, just log it
|
||||
console.log('Test navigation called with:', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a type-safe route object that can be passed to testNavigate
|
||||
* This function helps ensure that route creation is type-safe
|
||||
*
|
||||
* @example
|
||||
* const route = createRoute({
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: {
|
||||
* chatId: '123'
|
||||
* }
|
||||
* });
|
||||
* testNavigate(route);
|
||||
*
|
||||
* @example
|
||||
* // Use it in a function to return type-safe navigation options
|
||||
* const createMyRoute = () => {
|
||||
* return createRoute({
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: {
|
||||
* chatId: '123'
|
||||
* }
|
||||
* });
|
||||
* };
|
||||
*/
|
||||
export function createRoute<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
>(
|
||||
options: BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo>
|
||||
): BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo> {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type helper to extract the route options type for a specific route
|
||||
*
|
||||
* @example
|
||||
* type ChatRouteOptions = RouteOptions<'/app/chats/$chatId'>;
|
||||
* // This will give you the type with proper params like { chatId: string }
|
||||
*/
|
||||
export type RouteOptions<
|
||||
TTo extends FileRouteTypes['id'],
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
> = BusterNavigateOptions<TFrom, TTo>;
|
||||
|
||||
/**
|
||||
* Factory function to create route builders for specific routes
|
||||
* This provides even more type safety by locking in the route path
|
||||
*
|
||||
* @example
|
||||
* const createChatRoute = createRouteFactory('/app/chats/$chatId');
|
||||
* const route = createChatRoute({ chatId: '123' });
|
||||
*/
|
||||
export function createRouteFactory<
|
||||
TTo extends FileRouteTypes['id'],
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
>(to: TTo) {
|
||||
return (
|
||||
params: BusterNavigateOptions<TFrom, TTo> extends { params: infer P } ? P : never,
|
||||
options?: Omit<BusterNavigateOptions<TFrom, TTo>, 'to' | 'params'>
|
||||
): BusterNavigateOptions<TFrom, TTo> => {
|
||||
return {
|
||||
to,
|
||||
params,
|
||||
...options,
|
||||
} as BusterNavigateOptions<TFrom, TTo>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts navigation options to a URL string (href)
|
||||
* This replaces dynamic segments ($param) with actual parameter values
|
||||
*
|
||||
* @example
|
||||
* const navOptions = {
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: { chatId: '123' }
|
||||
* };
|
||||
* const href = navigationOptionsToHref(navOptions);
|
||||
* // Returns: '/app/chats/123'
|
||||
*
|
||||
* @example
|
||||
* // With multiple params
|
||||
* const complexNavOptions = {
|
||||
* to: '/app/chats/$chatId/dashboard/$dashboardId',
|
||||
* params: { chatId: '123', dashboardId: '456' }
|
||||
* };
|
||||
* const complexHref = navigationOptionsToHref(complexNavOptions);
|
||||
* // Returns: '/app/chats/123/dashboard/456'
|
||||
*/
|
||||
export function navigationOptionsToHref<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
>(options: BusterNavigateOptions<TFrom, TTo>): string {
|
||||
let href = options.to as string;
|
||||
|
||||
// Replace all $param placeholders with actual values from params
|
||||
if (options.params && typeof options.params === 'object') {
|
||||
Object.entries(options.params).forEach(([key, value]) => {
|
||||
// Replace $param with the actual value
|
||||
// Use a regex to ensure we only replace the exact $param pattern
|
||||
const pattern = new RegExp(`\\$${key}(?=/|$)`, 'g');
|
||||
href = href.replace(pattern, encodeURIComponent(String(value)));
|
||||
});
|
||||
}
|
||||
|
||||
// Add search params if they exist
|
||||
if (options.search && typeof options.search === 'object') {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(options.search).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
const searchString = searchParams.toString();
|
||||
if (searchString) {
|
||||
href += `?${searchString}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add hash if it exists
|
||||
if (options.hash) {
|
||||
href += `#${options.hash}`;
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand function to get href from navigation options
|
||||
* Alias for navigationOptionsToHref
|
||||
*/
|
||||
export const toHref = navigationOptionsToHref;
|
|
@ -0,0 +1,23 @@
|
|||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
import type { BusterNavigateOptions } from './types';
|
||||
|
||||
/**
|
||||
* Type-safe navigate function for testing that matches the behavior of useNavigate()
|
||||
* This function provides the same type safety as the regular navigate hook
|
||||
* by leveraging the RegisteredRouter type which contains all route definitions
|
||||
*/
|
||||
export function acceptsTypeSafeNavigateOptions<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
>(options: BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo>): void {
|
||||
// In a test environment, you might want to just log or store the navigation
|
||||
// For actual implementation, you could:
|
||||
// 1. Store the navigation in a test spy
|
||||
// 2. Update window.location in a test environment
|
||||
// 3. Use a mock router's navigate method
|
||||
|
||||
// For now, just log it
|
||||
console.log('Test navigation called with:', options);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
import type { BusterNavigateOptions } from './types';
|
||||
|
||||
/**
|
||||
* Creates a type-safe route object that can be passed to testNavigate
|
||||
* This function helps ensure that route creation is type-safe
|
||||
*
|
||||
* @example
|
||||
* const route = createRoute({
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: {
|
||||
* chatId: '123'
|
||||
* }
|
||||
* });
|
||||
* testNavigate(route);
|
||||
*
|
||||
* @example
|
||||
* // Use it in a function to return type-safe navigation options
|
||||
* const createMyRoute = () => {
|
||||
* return createRoute({
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: {
|
||||
* chatId: '123'
|
||||
* }
|
||||
* });
|
||||
* };
|
||||
*/
|
||||
export function createRoute<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
>(
|
||||
options: BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo>
|
||||
): BusterNavigateOptions<TFrom, TTo, TMaskFrom, TMaskTo> {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create route builders for specific routes
|
||||
* This provides even more type safety by locking in the route path
|
||||
*
|
||||
* @example
|
||||
* const createChatRoute = createRouteFactory('/app/chats/$chatId');
|
||||
* const route = createChatRoute({ chatId: '123' });
|
||||
*/
|
||||
export function createRouteFactory<
|
||||
TTo extends FileRouteTypes['id'],
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
>(to: TTo) {
|
||||
return (
|
||||
params: BusterNavigateOptions<TFrom, TTo> extends { params: infer P } ? P : never,
|
||||
options?: Omit<BusterNavigateOptions<TFrom, TTo>, 'to' | 'params'>
|
||||
): BusterNavigateOptions<TFrom, TTo> => {
|
||||
return {
|
||||
to,
|
||||
params,
|
||||
...options,
|
||||
} as BusterNavigateOptions<TFrom, TTo>;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export { acceptsTypeSafeNavigateOptions } from './acceptsTypeSafeNavigateOptions';
|
||||
export { createRoute, createRouteFactory } from './createRoute';
|
||||
export { routeToHref } from './navigationOptionsToHref';
|
||||
export type { BusterNavigateOptions, RouteOptions } from './types';
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { BusterNavigateOptions } from './typeSafeNavigation';
|
||||
import { navigationOptionsToHref, toHref } from './typeSafeNavigation';
|
||||
import type { BusterNavigateOptions } from '../tss-routes';
|
||||
import { routeToHref } from '../tss-routes';
|
||||
|
||||
describe('navigationOptionsToHref', () => {
|
||||
it('should convert simple route with single param', () => {
|
||||
|
@ -11,7 +11,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/123');
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/chat-123/dashboard/dash-456');
|
||||
});
|
||||
|
||||
|
@ -38,7 +38,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/chat-123/dashboard/dash-456/metrics/metric-789');
|
||||
});
|
||||
|
||||
|
@ -50,7 +50,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/chat%20with%20spaces%20%26%20special');
|
||||
});
|
||||
|
||||
|
@ -66,7 +66,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/123?filter=active&sort=date');
|
||||
});
|
||||
|
||||
|
@ -79,7 +79,7 @@ describe('navigationOptionsToHref', () => {
|
|||
hash: 'section-1',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/123#section-1');
|
||||
});
|
||||
|
||||
|
@ -95,7 +95,7 @@ describe('navigationOptionsToHref', () => {
|
|||
hash: 'privacy',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/123?tab=settings#privacy');
|
||||
});
|
||||
|
||||
|
@ -107,7 +107,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = toHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/metrics/metric-123');
|
||||
});
|
||||
|
||||
|
@ -116,7 +116,7 @@ describe('navigationOptionsToHref', () => {
|
|||
to: '/app/home',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/home');
|
||||
});
|
||||
|
||||
|
@ -130,7 +130,7 @@ describe('navigationOptionsToHref', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
const href = routeToHref(options);
|
||||
expect(href).toBe('/app/chats/123/report/report-456');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
import type { BusterNavigateOptions } from './types';
|
||||
|
||||
/**
|
||||
* Converts navigation options to a URL string (href)
|
||||
* This replaces dynamic segments ($param) with actual parameter values
|
||||
*
|
||||
* @example
|
||||
* const navOptions = {
|
||||
* to: '/app/chats/$chatId',
|
||||
* params: { chatId: '123' }
|
||||
* };
|
||||
* const href = navigationOptionsToHref(navOptions);
|
||||
* // Returns: '/app/chats/123'
|
||||
*
|
||||
* @example
|
||||
* // With multiple params
|
||||
* const complexNavOptions = {
|
||||
* to: '/app/chats/$chatId/dashboard/$dashboardId',
|
||||
* params: { chatId: '123', dashboardId: '456' }
|
||||
* };
|
||||
* const complexHref = navigationOptionsToHref(complexNavOptions);
|
||||
* // Returns: '/app/chats/123/dashboard/456'
|
||||
*/
|
||||
function navigationOptionsToHref<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
>(options: BusterNavigateOptions<TFrom, TTo>): string {
|
||||
let href = options.to as string;
|
||||
|
||||
// Replace all $param placeholders with actual values from params
|
||||
if (options.params && typeof options.params === 'object') {
|
||||
Object.entries(options.params).forEach(([key, value]) => {
|
||||
// Replace $param with the actual value
|
||||
// Use a regex to ensure we only replace the exact $param pattern
|
||||
const pattern = new RegExp(`\\$${key}(?=/|$)`, 'g');
|
||||
href = href.replace(pattern, encodeURIComponent(String(value)));
|
||||
});
|
||||
}
|
||||
|
||||
// Add search params if they exist
|
||||
if (options.search && typeof options.search === 'object') {
|
||||
const searchParams = new URLSearchParams();
|
||||
Object.entries(options.search).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
const searchString = searchParams.toString();
|
||||
if (searchString) {
|
||||
href += `?${searchString}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add hash if it exists
|
||||
if (options.hash) {
|
||||
href += `#${options.hash}`;
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand function to get href from navigation options
|
||||
* Alias for navigationOptionsToHref
|
||||
*/
|
||||
export const routeToHref = navigationOptionsToHref;
|
|
@ -4,7 +4,7 @@ import {
|
|||
createRoute,
|
||||
createRouteFactory,
|
||||
type RouteOptions,
|
||||
} from './typeSafeNavigation';
|
||||
} from '../tss-routes';
|
||||
|
||||
// Example 1: Using acceptsTypeSafeNavigateOptions
|
||||
acceptsTypeSafeNavigateOptions({
|
|
@ -0,0 +1,32 @@
|
|||
import type { NavigateOptions, RegisteredRouter } from '@tanstack/react-router';
|
||||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
|
||||
/**
|
||||
* Type representing navigation options that can be passed to testNavigate
|
||||
* This is useful for creating type-safe route objects
|
||||
*
|
||||
* IMPORTANT: Always provide type parameters when using this type!
|
||||
*
|
||||
* ❌ Bad: BusterNavigateOptions (no type safety)
|
||||
* ✅ Good: BusterNavigateOptions<'/', '/app/chats/$chatId'>
|
||||
*
|
||||
* Without type parameters, TypeScript won't enforce required params!
|
||||
*/
|
||||
export type BusterNavigateOptions<
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
TTo extends string | undefined = undefined,
|
||||
TMaskFrom extends FileRouteTypes['id'] = TFrom,
|
||||
TMaskTo extends string = '',
|
||||
> = NavigateOptions<RegisteredRouter, TFrom, TTo, TMaskFrom, TMaskTo>;
|
||||
|
||||
/**
|
||||
* Type helper to extract the route options type for a specific route
|
||||
*
|
||||
* @example
|
||||
* type ChatRouteOptions = RouteOptions<'/app/chats/$chatId'>;
|
||||
* // This will give you the type with proper params like { chatId: string }
|
||||
*/
|
||||
export type RouteOptions<
|
||||
TTo extends FileRouteTypes['id'],
|
||||
TFrom extends FileRouteTypes['id'] = '/',
|
||||
> = BusterNavigateOptions<TFrom, TTo>;
|
Loading…
Reference in New Issue