From 5eb570df622f4790cca50eef0a58fc3be80c4776 Mon Sep 17 00:00:00 2001 From: scarf Date: Fri, 25 Apr 2025 15:28:03 +0900 Subject: [PATCH 1/6] feat(router-devtools-core): add navigation link to route --- .../src/BaseTanStackRouterDevtoolsPanel.tsx | 104 ++++++++++++++---- .../src/NavigateButton.tsx | 25 +++++ .../router-devtools-core/src/useStyles.tsx | 25 +++++ 3 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 packages/router-devtools-core/src/NavigateButton.tsx diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index b05428c9260..aa42380351a 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -1,7 +1,7 @@ import { clsx as cx } from 'clsx' import { default as invariant } from 'tiny-invariant' import { rootRouteId, trimPath } from '@tanstack/router-core' -import { createMemo } from 'solid-js' +import { Show, createMemo } from 'solid-js' import { useDevtoolsOnClose } from './context' import { useStyles } from './useStyles' import useLocalStorage from './useLocalStorage' @@ -10,6 +10,7 @@ import { getRouteStatusColor, getStatusColor, multiSortBy } from './utils' import { AgeTicker } from './AgeTicker' // import type { DevtoolsPanelOptions } from './TanStackRouterDevtoolsPanel' +import { NavigateButton } from './NavigateButton' import type { AnyContext, AnyRoute, @@ -64,6 +65,28 @@ function Logo(props: any) { ) } +function NavigateLink(props: { + class?: string + left?: JSX.Element + children?: JSX.Element + right?: JSX.Element +}) { + return ( +
+ {props.left} +
{props.children}
+ {props.right} +
+ ) +} + function RouteComp({ routerState, router, @@ -126,6 +149,23 @@ function RouteComp({ } }) + const navigationTarget = createMemo(() => { + if (isRoot) return '/' // __root__ is same as / + if (!route.path) return undefined // no path to navigate to + + const matched = match() + const fillDynamicParam = (s: string): string | undefined => + matched?.params[s.slice(1)] + + // fill in dynamic params + const segments = (route.fullPath as string) + .split('/') + .map((s) => (s.startsWith('$') ? fillDynamicParam(s) : s)) + + // can only determine full path when all dynamic params are filled + return segments.every((s) => s != null) ? segments.join('/') : undefined + }) + return (
-
-
- - {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '} - - {param()} -
- -
+ + {(navigate) => } + + } + right={} + > + + {isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '} + + {param()} +
{route.children?.length ? (
@@ -406,7 +451,7 @@ export const BaseTanStackRouterDevtoolsPanel = {(routerState().pendingMatches?.length ? routerState().pendingMatches : routerState().matches - )?.map((match: any, i: any) => { + )?.map((match: any, _i: any) => { return (
- - {`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`} - + + } + right={} + > + + {`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`} + +
) })} @@ -457,10 +512,19 @@ export const BaseTanStackRouterDevtoolsPanel = styles().matchIndicator(getStatusColor(match)), )} /> - - {`${match.id}`} - - + + } + right={} + > + {`${match.id}`} +
) })} diff --git a/packages/router-devtools-core/src/NavigateButton.tsx b/packages/router-devtools-core/src/NavigateButton.tsx new file mode 100644 index 00000000000..8f9e5161fc5 --- /dev/null +++ b/packages/router-devtools-core/src/NavigateButton.tsx @@ -0,0 +1,25 @@ +import { useStyles } from './useStyles' +import type { AnyRouter, NavigateOptions } from '@tanstack/router-core' +import type { Accessor } from 'solid-js' + +interface Props extends NavigateOptions { + router: Accessor +} + +export function NavigateButton({ to, params, search, router }: Props) { + const styles = useStyles() + + return ( + + ) +} diff --git a/packages/router-devtools-core/src/useStyles.tsx b/packages/router-devtools-core/src/useStyles.tsx index 3ba7a9785dc..d1a12ad5753 100644 --- a/packages/router-devtools-core/src/useStyles.tsx +++ b/packages/router-devtools-core/src/useStyles.tsx @@ -370,6 +370,12 @@ const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { return classes }, + routesRowInner: css` + display: 'flex'; + align-items: 'center'; + flex-grow: 1; + min-width: 0; + `, routeParamInfo: css` color: ${colors.gray[400]}; font-size: ${fontSize.xs}; @@ -385,6 +391,9 @@ const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { code: css` font-size: ${fontSize.xs}; line-height: ${lineHeight['xs']}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `, matchesContainer: css` flex: 1 1 auto; @@ -579,6 +588,22 @@ const stylesFactory = (shadowDOMTarget?: ShadowRoot) => { width: ${size[2]}; height: ${size[2]}; `, + navigateButton: css` + background: none; + border: none; + padding: 0 0 0 4px; + margin: 0; + color: ${colors.gray[400]}; + font-size: ${fontSize.md}; + cursor: pointer; + line-height: 1; + vertical-align: middle; + margin-right: 0.5ch; + flex-shrink: 0; + &:hover { + color: ${colors.blue[300]}; + } + `, } } From e679befaa3b77f67a287747cf65b7251530bbf3b Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:35:32 +1200 Subject: [PATCH 2/6] refactor(router-devtools-core): use `interpolatePath` instead of rebuilding logic reuse the `interpolatePath` function from router-core instead of manually trying to insert params into `route.fullPath` --- .../src/BaseTanStackRouterDevtoolsPanel.tsx | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index aa42380351a..7d1a132ed84 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -1,6 +1,6 @@ import { clsx as cx } from 'clsx' import { default as invariant } from 'tiny-invariant' -import { rootRouteId, trimPath } from '@tanstack/router-core' +import { interpolatePath, rootRouteId, trimPath } from '@tanstack/router-core' import { Show, createMemo } from 'solid-js' import { useDevtoolsOnClose } from './context' import { useStyles } from './useStyles' @@ -150,20 +150,42 @@ function RouteComp({ }) const navigationTarget = createMemo(() => { - if (isRoot) return '/' // __root__ is same as / + if (isRoot) return undefined // rootRouteId has no path if (!route.path) return undefined // no path to navigate to - const matched = match() - const fillDynamicParam = (s: string): string | undefined => - matched?.params[s.slice(1)] + // flatten all params in the router state, into a single object + const allParams = matches() + .flatMap((m) => m.params) + .reduce((prev, curr) => { + const keys = Object.keys(curr) + for (const key of keys) { + if (prev[key] === undefined) { + prev[key] = curr[key] + } + } + return prev + }, {}) + + // interpolatePath is used by router-core to generate the `to` + // path for the navigate function in the router + // setting leaveWildcards and leaveParams to true + // allows us to see the full path with all params + // and wildcards if they are not filled + const interpolatedPath = interpolatePath({ + path: route.fullPath, + params: allParams, + leaveWildcards: true, + leaveParams: true, + decodeCharMap: router().pathParamsDecodeCharMap, + }).interpolatedPath - // fill in dynamic params - const segments = (route.fullPath as string) + // determine if navigation is possible based on whether or not the returned path + const canNavigate = interpolatedPath .split('/') - .map((s) => (s.startsWith('$') ? fillDynamicParam(s) : s)) + .filter(Boolean) + .every((s) => !s.startsWith('$')) - // can only determine full path when all dynamic params are filled - return segments.every((s) => s != null) ? segments.join('/') : undefined + return canNavigate ? interpolatedPath : undefined }) return ( From 3d035d5529b70e500dc9abe4f48949fe9a82af2f Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:18:08 +1200 Subject: [PATCH 3/6] apply alternate suggestion --- .../src/BaseTanStackRouterDevtoolsPanel.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index 7d1a132ed84..182b4cbc114 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -154,17 +154,7 @@ function RouteComp({ if (!route.path) return undefined // no path to navigate to // flatten all params in the router state, into a single object - const allParams = matches() - .flatMap((m) => m.params) - .reduce((prev, curr) => { - const keys = Object.keys(curr) - for (const key of keys) { - if (prev[key] === undefined) { - prev[key] = curr[key] - } - } - return prev - }, {}) + const allParams = Object.assign({}, ...matches().flatMap((m) => m.params)) // interpolatePath is used by router-core to generate the `to` // path for the navigate function in the router From 72c8b58428b458a4d3cc39204132157757f30db4 Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 30 Apr 2025 20:52:52 +1200 Subject: [PATCH 4/6] refactor(router-core,router-devtools-core): better checking for path params --- packages/router-core/src/path.ts | 7 +++++- .../src/BaseTanStackRouterDevtoolsPanel.tsx | 25 ++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index f2daff5104f..ff339a26141 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -216,6 +216,7 @@ interface InterpolatePathOptions { type InterPolatePathResult = { interpolatedPath: string usedParams: Record + isMissingParams: boolean // true if any params were not available when being looked up in the params object } export function interpolatePath({ path, @@ -238,6 +239,7 @@ export function interpolatePath({ } } + const missingKeys = new Set() const usedParams: Record = {} const interpolatedPath = joinPaths( interpolatedPathSegments.map((segment) => { @@ -250,6 +252,9 @@ export function interpolatePath({ if (segment.type === 'param') { const key = segment.value.substring(1) + if (typeof params[key] === 'undefined') { + missingKeys.add(key) + } usedParams[key] = params[key] if (leaveParams) { const value = encodeParam(segment.value) @@ -261,7 +266,7 @@ export function interpolatePath({ return segment.value }), ) - return { usedParams, interpolatedPath } + return { usedParams, interpolatedPath, isMissingParams: missingKeys.size > 0 } } function encodePathParam(value: string, decodeCharMap?: Map) { diff --git a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx index 182b4cbc114..a84f318bc6e 100644 --- a/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx +++ b/packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx @@ -154,28 +154,23 @@ function RouteComp({ if (!route.path) return undefined // no path to navigate to // flatten all params in the router state, into a single object - const allParams = Object.assign({}, ...matches().flatMap((m) => m.params)) + const allParams = Object.assign({}, ...matches().map((m) => m.params)) // interpolatePath is used by router-core to generate the `to` // path for the navigate function in the router - // setting leaveWildcards and leaveParams to true - // allows us to see the full path with all params - // and wildcards if they are not filled - const interpolatedPath = interpolatePath({ + const interpolated = interpolatePath({ path: route.fullPath, params: allParams, - leaveWildcards: true, - leaveParams: true, + leaveWildcards: false, + leaveParams: false, decodeCharMap: router().pathParamsDecodeCharMap, - }).interpolatedPath - - // determine if navigation is possible based on whether or not the returned path - const canNavigate = interpolatedPath - .split('/') - .filter(Boolean) - .every((s) => !s.startsWith('$')) + }) - return canNavigate ? interpolatedPath : undefined + // only if `interpolated` is not missing params, return the path since this + // means that all the params are present for a successful navigation + return !interpolated.isMissingParams + ? interpolated.interpolatedPath + : undefined }) return ( From 066e89dc1eae808d9328bc918ef4e44ea448b317 Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:03:00 +1200 Subject: [PATCH 5/6] refactor(router-core): better check-condition --- packages/router-core/src/path.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index ff339a26141..9a0eb2d2f53 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -252,7 +252,7 @@ export function interpolatePath({ if (segment.type === 'param') { const key = segment.value.substring(1) - if (typeof params[key] === 'undefined') { + if (!(key in params)) { missingKeys.add(key) } usedParams[key] = params[key] From b84ad17a2c7de1658295a55c3df02c4a3e95352e Mon Sep 17 00:00:00 2001 From: SeanCassiere <33615041+SeanCassiere@users.noreply.github.com> Date: Wed, 30 Apr 2025 21:07:24 +1200 Subject: [PATCH 6/6] refactor(router-core): short circuit and exit early --- packages/router-core/src/path.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/router-core/src/path.ts b/packages/router-core/src/path.ts index 9a0eb2d2f53..4d002673cdb 100644 --- a/packages/router-core/src/path.ts +++ b/packages/router-core/src/path.ts @@ -239,7 +239,10 @@ export function interpolatePath({ } } - const missingKeys = new Set() + // Tracking if any params are missing in the `params` object + // when interpolating the path + let isMissingParams = false + const usedParams: Record = {} const interpolatedPath = joinPaths( interpolatedPathSegments.map((segment) => { @@ -252,8 +255,8 @@ export function interpolatePath({ if (segment.type === 'param') { const key = segment.value.substring(1) - if (!(key in params)) { - missingKeys.add(key) + if (!isMissingParams && !(key in params)) { + isMissingParams = true } usedParams[key] = params[key] if (leaveParams) { @@ -266,7 +269,7 @@ export function interpolatePath({ return segment.value }), ) - return { usedParams, interpolatedPath, isMissingParams: missingKeys.size > 0 } + return { usedParams, interpolatedPath, isMissingParams } } function encodePathParam(value: string, decodeCharMap?: Map) {