Skip to content

Commit 8d65c01

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/flow-tests/test-frontend/vite-context-path/vite-6.0.13
2 parents 279ee8c + 0ea9cf2 commit 8d65c01

File tree

16 files changed

+595
-34
lines changed

16 files changed

+595
-34
lines changed

flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,19 @@ public boolean cancelPendingTitleUpdate() {
807807
return result;
808808
}
809809

810+
/**
811+
* Populate the routerTargetChain with RouterLayouts, but only if the target
812+
* chain is empty. If the chain contains elements the given list is ignored.
813+
*
814+
* @param layouts
815+
* stored router target chain to set as last navigated chain
816+
*/
817+
public void setRouterTargetChain(List<RouterLayout> layouts) {
818+
if (routerTargetChain.isEmpty()) {
819+
routerTargetChain.addAll(layouts);
820+
}
821+
}
822+
810823
/**
811824
* Shows a route target in the related UI. This method is intended for
812825
* framework use only. Use {@link UI#navigate(String)} to change the route

flow-server/src/main/java/com/vaadin/flow/router/PreserveOnRefresh.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,19 @@
4545
@Inherited
4646
@Documented
4747
public @interface PreserveOnRefresh {
48+
49+
/**
50+
* Set to true if refresh should also reuse partial chain components of
51+
* stored view chain.
52+
* <p>
53+
* This means that when navigating from a preserve on refresh target to a
54+
* new url in the same client window context, where windowName matches, the
55+
* router layouts that have been preserved will be reused without
56+
* re-creation for the new route.
57+
* <p>
58+
* Default is {@code false} so only url match is repopulated.
59+
*
60+
* @return {@code true} if partial chain match should be checked and used
61+
*/
62+
boolean partialMatch() default false;
4863
}

flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public NavigationState getNavigationState() {
129129
*/
130130
@SuppressWarnings("unchecked")
131131
// Non-private for testing purposes
132-
static <T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
132+
<T extends HasElement> T getRouteTarget(Class<T> routeTargetType,
133133
NavigationEvent event, boolean lastElement) {
134134
UI ui = event.getUI();
135135
Instantiator instantiator = Instantiator.get(ui);
@@ -267,6 +267,48 @@ private boolean populateChain(ArrayList<HasElement> chain,
267267
return true;
268268
}
269269
chain.addAll(maybeChain.get());
270+
271+
// If partialMatch is set to true check if the cache contains a
272+
// chain and possibly request extended details to get window name
273+
// to select cached chain.
274+
if (chain.isEmpty() && isPreservePartialTarget(
275+
navigationState.getNavigationTarget(), routeLayoutTypes)) {
276+
UI ui = event.getUI();
277+
if (ui.getInternals().getExtendedClientDetails() == null) {
278+
PreservedComponentCache cache = ui.getSession()
279+
.getAttribute(PreservedComponentCache.class);
280+
if (cache != null && !cache.isEmpty()) {
281+
// As there is a cached chain we get the client details
282+
// to get the window name so we can determine if the
283+
// cache contains a chain for us to use.
284+
ui.getPage().retrieveExtendedClientDetails(
285+
details -> handle(event));
286+
return true;
287+
}
288+
} else {
289+
Optional<List<HasElement>> partialChain = getWindowPreservedChain(
290+
ui.getSession(),
291+
ui.getInternals().getExtendedClientDetails()
292+
.getWindowName());
293+
if (partialChain.isPresent()) {
294+
List<HasElement> oldChain = partialChain.get();
295+
disconnectElements(oldChain, ui);
296+
297+
List<RouterLayout> routerLayouts = new ArrayList<>();
298+
299+
for (HasElement hasElement : oldChain) {
300+
if (hasElement instanceof RouterLayout) {
301+
routerLayouts.add((RouterLayout) hasElement);
302+
} else {
303+
// Remove any non element from their parent to
304+
// not get old or duplicate route content
305+
hasElement.getElement().removeFromParent();
306+
}
307+
}
308+
ui.getInternals().setRouterTargetChain(routerLayouts);
309+
}
310+
}
311+
}
270312
} else {
271313
// Create an empty chain which gets populated later in
272314
// `createChainIfEmptyAndExecuteBeforeEnterNavigation`.
@@ -966,23 +1008,7 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
9661008
if (maybePreserved.isPresent()) {
9671009
// Re-use preserved chain for this route
9681010
ArrayList<HasElement> chain = maybePreserved.get();
969-
final HasElement root = chain.get(chain.size() - 1);
970-
final Component component = (Component) chain.get(0);
971-
final Optional<UI> maybePrevUI = component.getUI();
972-
973-
if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
974-
return Optional.of(chain);
975-
}
976-
977-
// Remove the top-level component from the tree
978-
root.getElement().removeFromTree(false);
979-
980-
// Transfer all remaining UI child elements (typically dialogs
981-
// and notifications) to the new UI
982-
maybePrevUI.ifPresent(prevUi -> {
983-
ui.getInternals().moveElementsFrom(prevUi);
984-
prevUi.close();
985-
});
1011+
disconnectElements(chain, ui);
9861012

9871013
return Optional.of(chain);
9881014
}
@@ -991,6 +1017,26 @@ private Optional<ArrayList<HasElement>> getPreservedChain(
9911017
return Optional.of(new ArrayList<>(0));
9921018
}
9931019

1020+
private static void disconnectElements(List<HasElement> chain, UI ui) {
1021+
final HasElement root = chain.get(chain.size() - 1);
1022+
final Component component = (Component) chain.get(0);
1023+
final Optional<UI> maybePrevUI = component.getUI();
1024+
1025+
if (maybePrevUI.isPresent() && maybePrevUI.get().equals(ui)) {
1026+
return;
1027+
}
1028+
1029+
// Remove the top-level component from the tree
1030+
root.getElement().removeFromTree(false);
1031+
1032+
// Transfer all remaining UI child elements (typically dialogs
1033+
// and notifications) to the new UI
1034+
maybePrevUI.ifPresent(prevUi -> {
1035+
ui.getInternals().moveElementsFrom(prevUi);
1036+
prevUi.close();
1037+
});
1038+
}
1039+
9941040
/**
9951041
* Invoke this method with the chain that needs to be preserved after
9961042
* {@link #handle(NavigationEvent)} method created it.
@@ -1079,6 +1125,18 @@ private static boolean isPreserveOnRefreshTarget(
10791125
.isAnnotationPresent(PreserveOnRefresh.class));
10801126
}
10811127

1128+
private static boolean isPreservePartialTarget(
1129+
Class<? extends Component> routeTargetType,
1130+
List<Class<? extends RouterLayout>> routeLayoutTypes) {
1131+
return (routeTargetType.isAnnotationPresent(PreserveOnRefresh.class)
1132+
&& routeTargetType.getAnnotation(PreserveOnRefresh.class)
1133+
.partialMatch())
1134+
|| routeLayoutTypes.stream().anyMatch(layoutType -> layoutType
1135+
.isAnnotationPresent(PreserveOnRefresh.class)
1136+
&& layoutType.getAnnotation(PreserveOnRefresh.class)
1137+
.partialMatch());
1138+
}
1139+
10821140
// maps window.name to (location, chain)
10831141
private static class PreservedComponentCache
10841142
extends HashMap<String, Pair<String, ArrayList<HasElement>>> {
@@ -1105,9 +1163,27 @@ static Optional<ArrayList<HasElement>> getPreservedChain(
11051163
if (cache != null && cache.containsKey(windowName) && cache
11061164
.get(windowName).getFirst().equals(location.getPath())) {
11071165
return Optional.of(cache.get(windowName).getSecond());
1108-
} else {
1109-
return Optional.empty();
11101166
}
1167+
return Optional.empty();
1168+
}
1169+
1170+
/**
1171+
* Get a preserved chain by window name only ignoring location path.
1172+
*
1173+
* @param session
1174+
* current session
1175+
* @param windowName
1176+
* window name to get cached view stack for
1177+
* @return view stack cache if available for window name
1178+
*/
1179+
static Optional<List<HasElement>> getWindowPreservedChain(
1180+
VaadinSession session, String windowName) {
1181+
final PreservedComponentCache cache = session
1182+
.getAttribute(PreservedComponentCache.class);
1183+
if (cache != null && cache.containsKey(windowName)) {
1184+
return Optional.of(cache.get(windowName).getSecond());
1185+
}
1186+
return Optional.empty();
11111187
}
11121188

11131189
static void setPreservedChain(VaadinSession session, String windowName,

flow-server/src/test/java/com/vaadin/flow/server/menu/MenuRegistryTest.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public void getMenuItemsContainsExpectedClientPaths() throws IOException {
144144
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
145145
Files.writeString(clientFiles.toPath(), testClientRouteFile);
146146

147-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
147+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
148148
.getMenuItems(true);
149149

150150
Assert.assertEquals(10, menuItems.size());
@@ -158,7 +158,7 @@ public void getMenuItemsWithNestedFiltering_doesNotThrow()
158158
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
159159
Files.writeString(clientFiles.toPath(), nestedLoginRequiredRouteFile);
160160

161-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
161+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
162162
.getMenuItems(true);
163163

164164
Assert.assertEquals(0, menuItems.size());
@@ -171,7 +171,7 @@ public void getMenuItemsNoFilteringContainsAllClientPaths()
171171
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
172172
Files.writeString(clientFiles.toPath(), testClientRouteFile);
173173

174-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
174+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
175175
.getMenuItems(false);
176176

177177
Assert.assertEquals(13, menuItems.size());
@@ -191,7 +191,7 @@ public void testNonCollidingServerAndClientRoutesDoesNotThrow()
191191
Arrays.asList(MyRoute.class, MyInfo.class)
192192
.forEach(routeConfiguration::setAnnotatedRoute);
193193

194-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
194+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
195195
.getMenuItems(false);
196196
Assert.assertEquals(15, menuItems.size());
197197

@@ -218,6 +218,8 @@ public void productionMode_getMenuItemsContainsExpectedClientPaths()
218218
throws IOException {
219219
Mockito.when(deploymentConfiguration.isProductionMode())
220220
.thenReturn(true);
221+
// Clear any production mode execution route contents
222+
MenuRegistry.clearFileRoutesCache();
221223

222224
tmpDir.newFolder("META-INF", "VAADIN");
223225
File clientFiles = new File(tmpDir.getRoot(),
@@ -232,11 +234,14 @@ public void productionMode_getMenuItemsContainsExpectedClientPaths()
232234
menuRegistry.when(() -> MenuRegistry.getClassLoader())
233235
.thenReturn(mockClassLoader);
234236

235-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
237+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
236238
.getMenuItems(true);
237239

238240
Assert.assertEquals(10, menuItems.size());
239241
assertClientRoutes(menuItems);
242+
} finally {
243+
// Clear our routes from production mode cache
244+
MenuRegistry.clearFileRoutesCache();
240245
}
241246
}
242247

@@ -247,7 +252,7 @@ public void getMenuItemsContainsExpectedServerPaths() {
247252
Arrays.asList(MyRoute.class, MyInfo.class)
248253
.forEach(routeConfiguration::setAnnotatedRoute);
249254

250-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
255+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
251256
.getMenuItems(true);
252257

253258
Assert.assertEquals(2, menuItems.size());
@@ -266,7 +271,7 @@ public void getMenuItemsContainBothClientAndServerPaths()
266271
Arrays.asList(MyRoute.class, MyInfo.class)
267272
.forEach(routeConfiguration::setAnnotatedRoute);
268273

269-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
274+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
270275
.getMenuItems(true);
271276

272277
Assert.assertEquals(12, menuItems.size());
@@ -307,7 +312,7 @@ public void testWithLoggedInUser_userHasRoles() throws IOException {
307312
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
308313
Files.writeString(clientFiles.toPath(), testClientRouteFile);
309314

310-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
315+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
311316
.getMenuItems(true);
312317

313318
Assert.assertEquals(13, menuItems.size());
@@ -336,7 +341,7 @@ public void testWithLoggedInUser_noMatchingRoles() throws IOException {
336341
File clientFiles = new File(generated, FILE_ROUTES_JSON_NAME);
337342
Files.writeString(clientFiles.toPath(), testClientRouteFile);
338343

339-
Map<String, AvailableViewInfo> menuItems = new MenuRegistry()
344+
Map<String, AvailableViewInfo> menuItems = MenuRegistry
340345
.getMenuItems(true);
341346

342347
Assert.assertEquals(11, menuItems.size());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.misc.ui.partial;
18+
19+
import com.vaadin.flow.component.html.Div;
20+
import com.vaadin.flow.component.html.NativeButton;
21+
import com.vaadin.flow.router.ParentLayout;
22+
import com.vaadin.flow.router.RouterLayout;
23+
24+
@ParentLayout(RootLayout.class)
25+
public class MainLayout extends Div implements RouterLayout {
26+
27+
public static final String EVENT_LOG_ID = "event-log";
28+
public static final String RESET_ID = "reset-log";
29+
30+
private static int eventCounter = 0;
31+
32+
private final Div log = new Div();
33+
34+
public MainLayout() {
35+
log.setText(++eventCounter + ": " + getClass().getSimpleName()
36+
+ ": constructor");
37+
log.setId(EVENT_LOG_ID);
38+
NativeButton reset = new NativeButton("Reset count",
39+
e -> eventCounter = 0);
40+
reset.setId(RESET_ID);
41+
add(log, reset);
42+
}
43+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.misc.ui.partial;
18+
19+
import com.vaadin.flow.component.html.Div;
20+
import com.vaadin.flow.router.Route;
21+
import com.vaadin.flow.router.RouterLink;
22+
23+
@Route(value = "main", layout = MainLayout.class)
24+
public class MainView extends Div {
25+
26+
public MainView() {
27+
28+
add(new RouterLink("Navigate to second view - this works correctly",
29+
SecondView.class));
30+
}
31+
}

0 commit comments

Comments
 (0)