Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/router-core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ interface InterpolatePathOptions {
type InterPolatePathResult = {
interpolatedPath: string
usedParams: Record<string, unknown>
isMissingParams: boolean // true if any params were not available when being looked up in the params object
}
export function interpolatePath({
path,
Expand All @@ -238,6 +239,10 @@ export function interpolatePath({
}
}

// Tracking if any params are missing in the `params` object
// when interpolating the path
let isMissingParams = false

const usedParams: Record<string, unknown> = {}
const interpolatedPath = joinPaths(
interpolatedPathSegments.map((segment) => {
Expand All @@ -250,6 +255,9 @@ export function interpolatePath({

if (segment.type === 'param') {
const key = segment.value.substring(1)
if (!isMissingParams && !(key in params)) {
isMissingParams = true
}
usedParams[key] = params[key]
if (leaveParams) {
const value = encodeParam(segment.value)
Expand All @@ -261,7 +269,7 @@ export function interpolatePath({
return segment.value
}),
)
return { usedParams, interpolatedPath }
return { usedParams, interpolatedPath, isMissingParams }
}

function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
Expand Down
113 changes: 92 additions & 21 deletions packages/router-devtools-core/src/BaseTanStackRouterDevtoolsPanel.tsx
Original file line number Diff line number Diff line change
@@ -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 { interpolatePath, rootRouteId, trimPath } from '@tanstack/router-core'
import { Show, createMemo } from 'solid-js'
import { useDevtoolsOnClose } from './context'
import { useStyles } from './useStyles'
import useLocalStorage from './useLocalStorage'
Expand All @@ -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,
Expand Down Expand Up @@ -64,6 +65,28 @@ function Logo(props: any) {
)
}

function NavigateLink(props: {
class?: string
left?: JSX.Element
children?: JSX.Element
right?: JSX.Element
}) {
return (
<div
class={props.class}
style={{
display: 'flex',
'align-items': 'center',
width: '100%',
}}
>
{props.left}
<div style={{ 'flex-grow': 1, 'min-width': 0 }}>{props.children}</div>
{props.right}
</div>
)
}

function RouteComp({
routerState,
router,
Expand Down Expand Up @@ -126,6 +149,30 @@ function RouteComp({
}
})

const navigationTarget = createMemo<string | undefined>(() => {
if (isRoot) return undefined // rootRouteId has no path
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().map((m) => m.params))

// interpolatePath is used by router-core to generate the `to`
// path for the navigate function in the router
const interpolated = interpolatePath({
path: route.fullPath,
params: allParams,
leaveWildcards: false,
leaveParams: false,
decodeCharMap: router().pathParamsDecodeCharMap,
})

// 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 (
<div>
<div
Expand All @@ -145,15 +192,20 @@ function RouteComp({
styles().matchIndicator(getRouteStatusColor(matches(), route)),
)}
/>
<div class={cx(styles().routesRow(!!match()))}>
<div>
<code class={styles().code}>
{isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '}
</code>
<code class={styles().routeParamInfo}>{param()}</code>
</div>
<AgeTicker match={match()} router={router} />
</div>
<NavigateLink
class={cx(styles().routesRow(!!match()))}
left={
<Show when={navigationTarget()}>
{(navigate) => <NavigateButton to={navigate()} router={router} />}
</Show>
}
right={<AgeTicker match={match()} router={router} />}
>
<code class={styles().code}>
{isRoot ? rootRouteId : route.path || trimPath(route.id)}{' '}
</code>
<code class={styles().routeParamInfo}>{param()}</code>
</NavigateLink>
</div>
{route.children?.length ? (
<div class={styles().nestedRouteRow(!!isRoot)}>
Expand Down Expand Up @@ -406,7 +458,7 @@ export const BaseTanStackRouterDevtoolsPanel =
{(routerState().pendingMatches?.length
? routerState().pendingMatches
: routerState().matches
)?.map((match: any, i: any) => {
)?.map((match: any, _i: any) => {
return (
<div
role="button"
Expand All @@ -421,11 +473,21 @@ export const BaseTanStackRouterDevtoolsPanel =
styles().matchIndicator(getStatusColor(match)),
)}
/>

<code
class={styles().matchID}
>{`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}</code>
<AgeTicker match={match} router={router} />
<NavigateLink
left={
<NavigateButton
to={match.pathname}
params={match.params}
search={match.search}
router={router}
/>
}
right={<AgeTicker match={match} router={router} />}
>
<code class={styles().matchID}>
{`${match.routeId === rootRouteId ? rootRouteId : match.pathname}`}
</code>
</NavigateLink>
</div>
)
})}
Expand Down Expand Up @@ -457,10 +519,19 @@ export const BaseTanStackRouterDevtoolsPanel =
styles().matchIndicator(getStatusColor(match)),
)}
/>

<code class={styles().matchID}>{`${match.id}`}</code>

<AgeTicker match={match} router={router} />
<NavigateLink
left={
<NavigateButton
to={match.pathname}
params={match.params}
search={match.search}
router={router}
/>
}
right={<AgeTicker match={match} router={router} />}
>
<code class={styles().matchID}>{`${match.id}`}</code>
</NavigateLink>
</div>
)
})}
Expand Down
25 changes: 25 additions & 0 deletions packages/router-devtools-core/src/NavigateButton.tsx
Original file line number Diff line number Diff line change
@@ -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<AnyRouter>
}

export function NavigateButton({ to, params, search, router }: Props) {
const styles = useStyles()

return (
<button
type="button"
title={`Navigate to ${to}`}
class={styles().navigateButton}
onClick={(e) => {
e.stopPropagation()
router().navigate({ to, params, search })
}}
>
</button>
)
}
25 changes: 25 additions & 0 deletions packages/router-devtools-core/src/useStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -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]};
}
`,
}
}

Expand Down