diff --git a/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUtilTest.java b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUtilTest.java
index 6da3a03860..444b481025 100644
--- a/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUtilTest.java
+++ b/packages/java/endpoint/src/test/java/com/vaadin/hilla/route/RouteUtilTest.java
@@ -36,7 +36,7 @@ public void test_role_allowed() {
AvailableViewInfo config = new AvailableViewInfo("Test",
new String[] { "ROLE_ADMIN" }, false, "/test", false, false,
- null, null, null);
+ null, null, null, false);
routeUtil.setRoutes(Collections.singletonMap("/test", config));
Assert.assertTrue("Route should be allowed for ADMIN role.",
@@ -52,7 +52,7 @@ public void test_role_not_allowed() {
AvailableViewInfo config = new AvailableViewInfo("Test",
new String[] { "ROLE_ADMIN" }, false, "/test", false, false,
- null, null, null);
+ null, null, null, false);
routeUtil.setRoutes(Collections.singletonMap("/test", config));
Assert.assertFalse("USER role should not allow ADMIN route.",
@@ -67,7 +67,7 @@ public void test_login_required() {
request.setUserPrincipal(Mockito.mock(Principal.class));
AvailableViewInfo config = new AvailableViewInfo("Test", null, true,
- "/test", false, false, null, null, null);
+ "/test", false, false, null, null, null, false);
routeUtil.setRoutes(Collections.singletonMap("/test", config));
Assert.assertTrue("Request with user principal should be allowed",
@@ -82,7 +82,7 @@ public void test_login_required_failed() {
request.setUserPrincipal(null);
AvailableViewInfo config = new AvailableViewInfo("Test", null, true,
- "/test", false, false, null, null, null);
+ "/test", false, false, null, null, null, false);
routeUtil.setRoutes(Collections.singletonMap("/test", config));
Assert.assertFalse("No login should be denied access",
@@ -97,11 +97,11 @@ public void test_login_required_on_layout() {
request.setUserPrincipal(null);
AvailableViewInfo pageWithoutLogin = new AvailableViewInfo("Test Page",
- null, false, "/test", false, false, null, null, null);
+ null, false, "/test", false, false, null, null, null, false);
AvailableViewInfo layoutWithLogin = new AvailableViewInfo("Test Layout",
null, true, "", false, false, null,
- Collections.singletonList(pageWithoutLogin), null);
+ Collections.singletonList(pageWithoutLogin), null, false);
routeUtil.setRoutes(Map.ofEntries(entry("/test", pageWithoutLogin),
entry("", layoutWithLogin)));
@@ -118,11 +118,11 @@ public void test_login_required_on_page() {
request.setUserPrincipal(null);
AvailableViewInfo pageWithLogin = new AvailableViewInfo("Test Page",
- null, true, "/test", false, false, null, null, null);
+ null, true, "/test", false, false, null, null, null, false);
AvailableViewInfo layoutWithoutLogin = new AvailableViewInfo(
"Test Layout", null, false, "", false, false, null,
- Collections.singletonList(pageWithLogin), null);
+ Collections.singletonList(pageWithLogin), null, false);
routeUtil.setRoutes(Map.ofEntries(entry("/test", pageWithLogin),
entry("", layoutWithoutLogin)));
@@ -142,7 +142,7 @@ public void test_login_not_required_on_root() {
request.setUserPrincipal(null);
AvailableViewInfo config = new AvailableViewInfo("Root", null, false,
- "", false, false, null, null, null);
+ "", false, false, null, null, null, false);
routeUtil.setRoutes(Collections.singletonMap("", config));
Assert.assertTrue("Login no required should allow access",
diff --git a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-admin.json b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-admin.json
index 0bb1fa0c80..8b18d71b81 100644
--- a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-admin.json
+++ b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-admin.json
@@ -7,7 +7,8 @@
"route": "/foo",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/bar": {
"params": {},
@@ -17,7 +18,8 @@
"route": "/bar",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/home": {
"params": {},
@@ -27,7 +29,8 @@
"route": "/home",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/wildcard": {
"params": {
@@ -39,7 +42,8 @@
"route": "/wildcard",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/comments": {
"params": {
@@ -51,7 +55,8 @@
"route": "/comments",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/profile": {
"params": {},
@@ -64,7 +69,8 @@
"route": "/profile",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/": {
"params": {},
@@ -74,7 +80,8 @@
"route": "/",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/orders": {
"params": {},
@@ -84,6 +91,7 @@
"route": "/orders",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
}
}
diff --git a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-anonymous.json b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-anonymous.json
index 9f2bce8153..a51782d242 100644
--- a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-anonymous.json
+++ b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-anonymous.json
@@ -7,7 +7,8 @@
"route": "/home",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/foo": {
"params": {},
@@ -17,7 +18,8 @@
"route": "/foo",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/wildcard": {
"params": {
@@ -29,7 +31,8 @@
"route": "/wildcard",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/bar": {
"params": {},
@@ -39,7 +42,8 @@
"route": "/bar",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/comments": {
"params": {
@@ -51,7 +55,8 @@
"route": "/comments",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/": {
"params": {},
@@ -61,7 +66,8 @@
"route": "/",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/orders": {
"params": {},
@@ -71,6 +77,7 @@
"route": "/orders",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
}
}
diff --git a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-user.json b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-user.json
index 0bb1fa0c80..8b18d71b81 100644
--- a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-user.json
+++ b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/available-views-user.json
@@ -7,7 +7,8 @@
"route": "/foo",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/bar": {
"params": {},
@@ -17,7 +18,8 @@
"route": "/bar",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/home": {
"params": {},
@@ -27,7 +29,8 @@
"route": "/home",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/wildcard": {
"params": {
@@ -39,7 +42,8 @@
"route": "/wildcard",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/comments": {
"params": {
@@ -51,7 +55,8 @@
"route": "/comments",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/profile": {
"params": {},
@@ -64,7 +69,8 @@
"route": "/profile",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/": {
"params": {},
@@ -74,7 +80,8 @@
"route": "/",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/orders": {
"params": {},
@@ -84,6 +91,7 @@
"route": "/orders",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
}
}
diff --git a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/only-client-views.json b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/only-client-views.json
index cc28c73ceb..c25c900683 100644
--- a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/only-client-views.json
+++ b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/only-client-views.json
@@ -7,7 +7,8 @@
"route": "/home",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/profile": {
"params": {},
@@ -20,7 +21,8 @@
"route": "/profile",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/": {
"params": {},
@@ -30,7 +32,8 @@
"route": "/",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/orders": {
"params": {},
@@ -40,6 +43,7 @@
"route": "/orders",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
}
}
diff --git a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/server-and-client-views.json b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/server-and-client-views.json
index 6ee29cb291..bb5dad1178 100644
--- a/packages/java/endpoint/src/test/resources/META-INF/VAADIN/server-and-client-views.json
+++ b/packages/java/endpoint/src/test/resources/META-INF/VAADIN/server-and-client-views.json
@@ -7,7 +7,8 @@
"route": "/foo",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/bar": {
"params": {},
@@ -17,7 +18,8 @@
"route": "/bar",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/home": {
"params": {},
@@ -27,7 +29,8 @@
"route": "/home",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/comments": {
"params": {
@@ -39,7 +42,8 @@
"route": "/comments",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/wildcard": {
"params": {
@@ -51,7 +55,8 @@
"route": "/wildcard",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
},
"/profile": {
"params": {},
@@ -64,6 +69,7 @@
"route": "/profile",
"lazy": false,
"register": false,
- "menu": null
+ "menu": null,
+ "flowLayout":false
}
}
diff --git a/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts b/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts
index 396339a55d..693b073da5 100644
--- a/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts
+++ b/packages/ts/file-router/src/runtime/RouterConfigurationBuilder.ts
@@ -143,6 +143,51 @@ export class RouterConfigurationBuilder {
return this;
}
+ /**
+ * Adds the layoutComponent as the parent layout to views with the flowLayouts ViewConfiguration set.
+ *
+ * @param layoutComponent - layout component to use, usually Flow
+ */
+ withLayout(layoutComponent: ComponentType): this {
+ function applyLayouts(routes: readonly RouteObject[]): readonly RouteObject[] {
+ const nestedRoutes = routes.map((route) => {
+ if (route.children === undefined) {
+ return route;
+ }
+
+ return {
+ ...route,
+ children: applyLayouts(route.children),
+ } as RouteObject;
+ });
+ return [
+ {
+ element: createElement(layoutComponent),
+ children: nestedRoutes,
+ },
+ ];
+ }
+
+ this.#modifiers.push((routes: readonly RouteObject[] | undefined) => {
+ if (!routes) {
+ return routes;
+ }
+ const withLayout = routes.filter((route) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ const layout = typeof route.handle === 'object' && 'flowLayout' in route.handle && route.handle.flowLayout;
+ return layout;
+ });
+ const allRoutes = routes.filter((route) => !withLayout.includes(route));
+ const catchAll = [routes.find((route) => route.path === '*')].filter((route) => route !== undefined);
+ withLayout.push(...catchAll); // Add * fallback to all child routes
+
+ allRoutes.unshift(...applyLayouts(withLayout));
+ return allRoutes;
+ });
+
+ return this;
+ }
+
/**
* Protects all the routes that require authentication. For more details see
* {@link @vaadin/hilla-react-auth#protectRoutes} function.
diff --git a/packages/ts/file-router/src/types.d.ts b/packages/ts/file-router/src/types.d.ts
index 819937dbee..deff5b0436 100644
--- a/packages/ts/file-router/src/types.d.ts
+++ b/packages/ts/file-router/src/types.d.ts
@@ -25,6 +25,12 @@ export type ViewConfig = Readonly<{
*/
route?: string;
+ /**
+ * Set to true to indicate that the view is using server side parent layout
+ * annotated with the Layout annotation.
+ */
+ flowLayout?: boolean;
+
menu?: Readonly<{
/**
* Title to use in the menu. Falls back the title property of the view
diff --git a/packages/ts/file-router/test/runtime/RouterConfigurationBuilder.spec.tsx b/packages/ts/file-router/test/runtime/RouterConfigurationBuilder.spec.tsx
index b3f955f48b..edb3480c74 100644
--- a/packages/ts/file-router/test/runtime/RouterConfigurationBuilder.spec.tsx
+++ b/packages/ts/file-router/test/runtime/RouterConfigurationBuilder.spec.tsx
@@ -1,5 +1,6 @@
import { expect, use } from '@esm-bundle/chai';
import chaiLike from 'chai-like';
+import { createElement } from 'react';
import sinonChai from 'sinon-chai';
import { RouterConfigurationBuilder } from '../../src/runtime/RouterConfigurationBuilder.js';
import { mockDocumentBaseURI } from '../mocks/dom.js';
@@ -81,6 +82,89 @@ describe('RouterBuilder', () => {
]);
});
+ it('should add layout routes under layout component', () => {
+ const serverWildcard = {
+ path: '*',
+ element: