diff --git a/apps/web-tss/src/lib/assets/assetParamsToRoute.test.ts b/apps/web-tss/src/lib/assets/assetParamsToRoute.test.ts
index c131c3469..385fb8666 100644
--- a/apps/web-tss/src/lib/assets/assetParamsToRoute.test.ts
+++ b/apps/web-tss/src/lib/assets/assetParamsToRoute.test.ts
@@ -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', () => {
diff --git a/apps/web-tss/src/lib/assets/assetParamsToRoute.usage.example.tsx b/apps/web-tss/src/lib/assets/assetParamsToRoute.usage.example.tsx
index b472ddf81..55eada443 100644
--- a/apps/web-tss/src/lib/assets/assetParamsToRoute.usage.example.tsx
+++ b/apps/web-tss/src/lib/assets/assetParamsToRoute.usage.example.tsx
@@ -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 (
);
}
@@ -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 View {params.assetType};
+ // You can use either Link component or native anchor
+ return (
+ <>
+ {/* Option 1: TanStack Router Link */}
+ View {params.assetType}
+
+ {/* Option 2: Native anchor with href */}
+
+ View {params.assetType} (native)
+
+ >
+ );
+}
+
+// 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 };
}
diff --git a/apps/web-tss/src/lib/assets/navigationOptionsToHref.test.ts b/apps/web-tss/src/lib/assets/navigationOptionsToHref.test.ts
new file mode 100644
index 000000000..deb7369ab
--- /dev/null
+++ b/apps/web-tss/src/lib/assets/navigationOptionsToHref.test.ts
@@ -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');
+ });
+});
diff --git a/apps/web-tss/src/lib/assets/typeSafeNavigation.ts b/apps/web-tss/src/lib/assets/typeSafeNavigation.ts
index 418a5fbd8..3121b508b 100644
--- a/apps/web-tss/src/lib/assets/typeSafeNavigation.ts
+++ b/apps/web-tss/src/lib/assets/typeSafeNavigation.ts
@@ -110,3 +110,68 @@ export function createRouteFactory<
} as BusterNavigateOptions;
};
}
+
+/**
+ * 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): 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;