Skip to content

Commit 5518bee

Browse files
authored
feat: add fallback functionality to navigation (#7078)
1 parent 9dc2937 commit 5518bee

File tree

10 files changed

+587
-2
lines changed

10 files changed

+587
-2
lines changed

.changeset/swift-games-sip.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@refinedev/nextjs-router": patch
3+
"@refinedev/react-router": patch
4+
"@refinedev/remix-router": patch
5+
---
6+
7+
feat: added `fallbackTo` prop to `NavigateToResource` component #7077
8+
9+
Now with `fallbackTo` prop, you can specify a fallback route when no resource is found to navigate to. The component will navigate to the provided fallback path instead of doing nothing, providing better user experience.
10+
11+
Resolves #7077

documentation/docs/routing/integrations/next-js/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ export default function IndexPage() {
480480

481481
`resource` (optional) - The name of the resource to navigate to. It will redirect to the first `list` route in the `resources` array if not provided.
482482

483+
`fallbackTo` (optional) - The path to navigate to if no resource is found. If not provided and no resource is found, no navigation will be made.
484+
483485
`meta` (optional) - The meta object to use if the route has parameters in it. The parameters in the current location will also be used to compose the route but `meta` will take precedence.
484486

485487
### UnsavedChangesNotifier

documentation/docs/routing/integrations/react-router/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,8 @@ const App = () => {
658658

659659
`resource` (optional) - The name of the resource to navigate to. It will redirect to the first `list` route in the `resources` array if not provided.
660660

661+
`fallbackTo` (optional) - The path to navigate to if no resource is found. If not provided and no resource is found, no navigation will be made.
662+
661663
`meta` (optional) - The meta object to use if the route has parameters in it. The parameters in the current location will also be used to compose the route but `meta` will take precedence.
662664

663665
### UnsavedChangesNotifier

documentation/docs/routing/integrations/remix/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,8 @@ export default function IndexPage() {
351351

352352
`resource` (optional) - The name of the resource to navigate to. It will redirect to the first `list` route in the `resources` array if not provided.
353353

354+
`fallbackTo` (optional) - The path to navigate to if no resource is found. If not provided and no resource is found, no navigation will be made.
355+
354356
`meta` (optional) - The meta object to use if the route has parameters in it. The parameters in the current location will also be used to compose the route but `meta` will take precedence.
355357

356358
### UnsavedChangesNotifier
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React from "react";
2+
import { vi } from "vitest";
3+
4+
import { NavigateToResource } from "./navigate-to-resource";
5+
import { render, TestWrapper, type ITestWrapperProps } from "../test/index";
6+
import { mockRouterProvider } from "../test/dataMocks";
7+
8+
const mockReplace = vi.fn();
9+
10+
vi.mock("next/navigation", () => ({
11+
useRouter: () => ({
12+
replace: mockReplace,
13+
}),
14+
}));
15+
16+
const renderNavigateToResource = (
17+
props: React.ComponentProps<typeof NavigateToResource> = {},
18+
wrapperProps: ITestWrapperProps = {},
19+
) => {
20+
return render(<NavigateToResource {...props} />, {
21+
wrapper: TestWrapper(wrapperProps),
22+
});
23+
};
24+
25+
describe("NavigateToResource", () => {
26+
beforeEach(() => {
27+
mockReplace.mockClear();
28+
});
29+
30+
it("should navigate to the first resource with list action", async () => {
31+
renderNavigateToResource(
32+
{},
33+
{
34+
resources: [
35+
{ name: "posts", list: "/posts" },
36+
{ name: "categories", list: "/categories" },
37+
],
38+
routerProvider: mockRouterProvider({
39+
fns: {
40+
go: () => {
41+
return ({ to, type }) => {
42+
if (type === "path") return to;
43+
return undefined;
44+
};
45+
},
46+
},
47+
}),
48+
},
49+
);
50+
51+
expect(mockReplace).toHaveBeenCalledWith("/posts");
52+
});
53+
54+
it("should navigate to the specified resource", async () => {
55+
renderNavigateToResource(
56+
{ resource: "categories" },
57+
{
58+
resources: [
59+
{ name: "posts", list: "/posts" },
60+
{ name: "categories", list: "/categories" },
61+
],
62+
routerProvider: mockRouterProvider({
63+
fns: {
64+
go: () => {
65+
return ({ to, type }) => {
66+
if (type === "path") return to;
67+
return undefined;
68+
};
69+
},
70+
},
71+
}),
72+
},
73+
);
74+
75+
expect(mockReplace).toHaveBeenCalledWith("/categories");
76+
});
77+
78+
it("should navigate to fallbackTo when no resource is found", async () => {
79+
const consoleWarnSpy = vi
80+
.spyOn(console, "warn")
81+
.mockImplementation(() => {});
82+
83+
renderNavigateToResource(
84+
{ fallbackTo: "/dashboard" },
85+
{
86+
resources: [],
87+
routerProvider: mockRouterProvider({
88+
fns: {
89+
go: () => {
90+
return ({ to, type }) => {
91+
if (type === "path") return to;
92+
return undefined;
93+
};
94+
},
95+
},
96+
}),
97+
},
98+
);
99+
100+
expect(consoleWarnSpy).toHaveBeenCalledWith(
101+
"No resource is found. navigation to /dashboard.",
102+
);
103+
expect(mockReplace).toHaveBeenCalledWith("/dashboard");
104+
105+
consoleWarnSpy.mockRestore();
106+
});
107+
108+
it("should not navigate when no resource and no fallbackTo is provided", async () => {
109+
renderNavigateToResource(
110+
{},
111+
{
112+
resources: [],
113+
routerProvider: mockRouterProvider({
114+
fns: {
115+
go: () => {
116+
return ({ to, type }) => {
117+
if (type === "path") return to;
118+
return undefined;
119+
};
120+
},
121+
},
122+
}),
123+
},
124+
);
125+
126+
expect(mockReplace).not.toHaveBeenCalled();
127+
});
128+
129+
it("should pass meta to getToPath", async () => {
130+
const meta = { foo: "bar" };
131+
132+
renderNavigateToResource(
133+
{ resource: "posts", meta },
134+
{
135+
resources: [{ name: "posts", list: "/posts" }],
136+
routerProvider: mockRouterProvider({
137+
fns: {
138+
go: () => {
139+
return ({ to, type }) => {
140+
if (type === "path") return to;
141+
return undefined;
142+
};
143+
},
144+
},
145+
}),
146+
},
147+
);
148+
149+
expect(mockReplace).toHaveBeenCalledWith("/posts");
150+
});
151+
152+
it("should only navigate once", async () => {
153+
const { rerender } = renderNavigateToResource(
154+
{},
155+
{
156+
resources: [{ name: "posts", list: "/posts" }],
157+
routerProvider: mockRouterProvider({
158+
fns: {
159+
go: () => {
160+
return ({ to, type }) => {
161+
if (type === "path") return to;
162+
return undefined;
163+
};
164+
},
165+
},
166+
}),
167+
},
168+
);
169+
170+
expect(mockReplace).toHaveBeenCalledTimes(1);
171+
172+
rerender(<NavigateToResource />);
173+
174+
expect(mockReplace).toHaveBeenCalledTimes(1);
175+
});
176+
177+
it("should prefer specified resource over first resource with list", async () => {
178+
renderNavigateToResource(
179+
{ resource: "categories" },
180+
{
181+
resources: [
182+
{ name: "posts", list: "/posts" },
183+
{ name: "categories", list: "/categories" },
184+
],
185+
routerProvider: mockRouterProvider({
186+
fns: {
187+
go: () => {
188+
return ({ to, type }) => {
189+
if (type === "path") return to;
190+
return undefined;
191+
};
192+
},
193+
},
194+
}),
195+
},
196+
);
197+
198+
expect(mockReplace).toHaveBeenCalledWith("/categories");
199+
expect(mockReplace).not.toHaveBeenCalledWith("/posts");
200+
});
201+
});

packages/nextjs-router/src/app/navigate-to-resource.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { useRouter } from "next/navigation";
44

55
type NavigateToResourceProps = {
66
resource?: string;
7+
fallbackTo?: string;
78
meta?: Record<string, unknown>;
89
};
910

1011
export const NavigateToResource = ({
1112
resource: resourceProp,
13+
fallbackTo,
1214
meta,
1315
}: NavigateToResourceProps) => {
1416
const ran = React.useRef(false);
@@ -35,6 +37,10 @@ export const NavigateToResource = ({
3537
}
3638
ran.current = true;
3739
}
40+
} else if (fallbackTo) {
41+
console.warn(`No resource is found. navigation to ${fallbackTo}.`);
42+
replace(fallbackTo);
43+
ran.current = true;
3844
}
3945
}, [toResource, replace, meta, getToPath]);
4046

0 commit comments

Comments
 (0)