mirror of https://github.com/buster-so/buster.git
Update typesafe navigation stuff
This commit is contained in:
parent
45e09d4603
commit
776be1a68a
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { FileRouteTypes } from '@/routeTree.gen';
|
||||
import { assetParamsToRoute, createRouteBuilder } from './assetParamsToRoute';
|
||||
|
||||
|
@ -87,7 +87,12 @@ describe('assetParamsToRoute', () => {
|
|||
assetType: 'chat',
|
||||
assetId: 'chat-123',
|
||||
});
|
||||
expect(chatRoute).toBe('/app/chats/$chatId');
|
||||
expect(chatRoute).toEqual({
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
},
|
||||
});
|
||||
|
||||
// Chat with dashboard
|
||||
const chatDashRoute = assetParamsToRoute({
|
||||
|
@ -95,7 +100,13 @@ describe('assetParamsToRoute', () => {
|
|||
assetId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
});
|
||||
expect(chatDashRoute).toBe('/app/chats/$chatId/dashboard/$dashboardId');
|
||||
expect(chatDashRoute).toEqual({
|
||||
to: '/app/chats/$chatId/dashboard/$dashboardId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
},
|
||||
});
|
||||
|
||||
// Chat with metric
|
||||
const chatMetricRoute = assetParamsToRoute({
|
||||
|
@ -103,7 +114,13 @@ describe('assetParamsToRoute', () => {
|
|||
assetId: 'chat-123',
|
||||
metricId: 'metric-456',
|
||||
});
|
||||
expect(chatMetricRoute).toBe('/app/chats/$chatId/metrics/$metricId');
|
||||
expect(chatMetricRoute).toEqual({
|
||||
to: '/app/chats/$chatId/metrics/$metricId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
metricId: 'metric-456',
|
||||
},
|
||||
});
|
||||
|
||||
// Chat with dashboard and metric
|
||||
const chatDashMetricRoute = assetParamsToRoute({
|
||||
|
@ -112,9 +129,14 @@ describe('assetParamsToRoute', () => {
|
|||
dashboardId: 'dash-456',
|
||||
metricId: 'metric-789',
|
||||
});
|
||||
expect(chatDashMetricRoute).toBe(
|
||||
'/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId'
|
||||
);
|
||||
expect(chatDashMetricRoute).toEqual({
|
||||
to: '/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
metricId: 'metric-789',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle metric asset type correctly', () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
assetParamsToRoute,
|
||||
assetParamsToRoutePath,
|
||||
} from './assetParamsToRoute';
|
||||
import { navigationOptionsToHref, toHref } from './typeSafeNavigation';
|
||||
|
||||
// Example React component showing usage
|
||||
export function AssetNavigationExample() {
|
||||
|
@ -50,6 +51,21 @@ export function AssetNavigationExample() {
|
|||
});
|
||||
console.log(justThePath); // '/app/chats/$chatId/report/$reportId'
|
||||
|
||||
// Example 5: Converting navigation options to href
|
||||
const reportNavOptions = assetParamsToRoute({
|
||||
assetType: 'report',
|
||||
assetId: 'report-999',
|
||||
chatId: 'chat-123',
|
||||
});
|
||||
|
||||
// Convert to actual URL
|
||||
const reportHref = navigationOptionsToHref(reportNavOptions);
|
||||
console.log(reportHref); // '/app/chats/chat-123/report/report-999'
|
||||
|
||||
// Example 6: Using with native anchor tags
|
||||
const dashboardHref = toHref(dashboardNavOptions);
|
||||
// dashboardHref is: '/app/chats/chat-123/dashboard/dash-789/metrics/metric-456'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button type="button" onClick={handleNavigateToChat}>
|
||||
|
@ -66,6 +82,14 @@ export function AssetNavigationExample() {
|
|||
<Link to={dashboardNavOptions.to} params={dashboardNavOptions.params}>
|
||||
Go to Dashboard (explicit)
|
||||
</Link>
|
||||
|
||||
{/* Native anchor tag with href */}
|
||||
<a href={dashboardHref}>Go to Dashboard (native anchor)</a>
|
||||
|
||||
{/* Example with custom styling or external link behavior */}
|
||||
<a href={reportHref} target="_blank" rel="noopener noreferrer" className="external-link">
|
||||
Open Report in New Tab
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -73,6 +97,43 @@ export function AssetNavigationExample() {
|
|||
// Example function that accepts AssetParamsToRoute
|
||||
export function createAssetLink(params: AssetParamsToRoute) {
|
||||
const navOptions = assetParamsToRoute(params);
|
||||
const href = toHref(navOptions);
|
||||
|
||||
return <Link {...navOptions}>View {params.assetType}</Link>;
|
||||
// You can use either Link component or native anchor
|
||||
return (
|
||||
<>
|
||||
{/* Option 1: TanStack Router Link */}
|
||||
<Link {...navOptions}>View {params.assetType}</Link>
|
||||
|
||||
{/* Option 2: Native anchor with href */}
|
||||
<a href={href} className="asset-link">
|
||||
View {params.assetType} (native)
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Example: Creating hrefs for external use (emails, sharing, etc.)
|
||||
export function generateShareableLinks() {
|
||||
const chatOptions = assetParamsToRoute({
|
||||
assetType: 'chat',
|
||||
assetId: 'chat-123',
|
||||
});
|
||||
|
||||
const metricOptions = assetParamsToRoute({
|
||||
assetType: 'metric',
|
||||
assetId: 'metric-456',
|
||||
chatId: 'chat-123',
|
||||
});
|
||||
|
||||
// Convert to absolute URLs for sharing
|
||||
const baseUrl = window.location.origin;
|
||||
const chatHref = baseUrl + toHref(chatOptions);
|
||||
const metricHref = baseUrl + toHref(metricOptions);
|
||||
|
||||
console.log('Share these links:');
|
||||
console.log('Chat:', chatHref); // https://example.com/app/chats/chat-123
|
||||
console.log('Metric:', metricHref); // https://example.com/app/chats/chat-123/metrics/metric-456
|
||||
|
||||
return { chatHref, metricHref };
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { BusterNavigateOptions } from './typeSafeNavigation';
|
||||
import { navigationOptionsToHref, toHref } from './typeSafeNavigation';
|
||||
|
||||
describe('navigationOptionsToHref', () => {
|
||||
it('should convert simple route with single param', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: '123',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/123');
|
||||
});
|
||||
|
||||
it('should convert route with multiple params', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId/dashboard/$dashboardId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/chat-123/dashboard/dash-456');
|
||||
});
|
||||
|
||||
it('should convert complex nested route', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId/dashboard/$dashboardId/metrics/$metricId',
|
||||
params: {
|
||||
chatId: 'chat-123',
|
||||
dashboardId: 'dash-456',
|
||||
metricId: 'metric-789',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/chat-123/dashboard/dash-456/metrics/metric-789');
|
||||
});
|
||||
|
||||
it('should handle URL encoding for special characters', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: 'chat with spaces & special',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/chat%20with%20spaces%20%26%20special');
|
||||
});
|
||||
|
||||
it('should add search params when provided', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: '123',
|
||||
},
|
||||
search: {
|
||||
filter: 'active',
|
||||
sort: 'date',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/123?filter=active&sort=date');
|
||||
});
|
||||
|
||||
it('should add hash when provided', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: '123',
|
||||
},
|
||||
hash: 'section-1',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/123#section-1');
|
||||
});
|
||||
|
||||
it('should handle search params and hash together', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId',
|
||||
params: {
|
||||
chatId: '123',
|
||||
},
|
||||
search: {
|
||||
tab: 'settings',
|
||||
},
|
||||
hash: 'privacy',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/123?tab=settings#privacy');
|
||||
});
|
||||
|
||||
it('should work with toHref alias', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/metrics/$metricId',
|
||||
params: {
|
||||
metricId: 'metric-123',
|
||||
},
|
||||
};
|
||||
|
||||
const href = toHref(options);
|
||||
expect(href).toBe('/app/metrics/metric-123');
|
||||
});
|
||||
|
||||
it('should handle routes without params', () => {
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/home',
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/home');
|
||||
});
|
||||
|
||||
it('should handle params that appear multiple times in different positions', () => {
|
||||
// Test with a route that has repeated param patterns
|
||||
const options: BusterNavigateOptions = {
|
||||
to: '/app/chats/$chatId/report/$reportId',
|
||||
params: {
|
||||
chatId: '123',
|
||||
reportId: 'report-456',
|
||||
},
|
||||
};
|
||||
|
||||
const href = navigationOptionsToHref(options);
|
||||
expect(href).toBe('/app/chats/123/report/report-456');
|
||||
});
|
||||
});
|
|
@ -110,3 +110,68 @@ export function createRouteFactory<
|
|||
} 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;
|
||||
|
|
Loading…
Reference in New Issue