When developing many Jmix based applications it might be convenient not to repeat yourself too often.
I did some development on Jmix applications with left and some with top navigation as well with similar basic behaviour. So I created a basic main view with user support extending the StandardMain view saving some duplicate code. This helps reducing templated code and reduce migration efforts.
A suggestion
import java.io.Serial;
import java.util.Objects;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.avatar.AvatarVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.core.userdetails.UserDetails;
import io.jmix.core.Messages;
import io.jmix.core.usersubstitution.CurrentUserSubstitution;
import io.jmix.flowui.UiComponents;
import io.jmix.flowui.app.main.StandardMainView;
/**
* Common implementation for different main view variants offering basic user menu support.
*
* @param <U>
* A user type.
*/
public class BasicMainView<U extends UserDetails> extends StandardMainView
{
/**
* Defines a default <i>Vaadin</i> route (path).
*/
public static final String ROUTE_PATH = "";
/**
* Generated serialization identifier.
*/
@Serial
private static final long serialVersionUID = -4485091503059586394L;
/**
* Holds the constructor injected <i>Jmix</i> core messages component.
*/
protected final Messages messages;
/**
* Holds the constructor injected <i>Jmix</i> core current-user-substitution component.
*/
protected final CurrentUserSubstitution currentUserSubstitution;
/**
* Holds the constructor injected <i>Jmix</i> (flow) UI components component.
*/
protected final UiComponents uiComponents;
/**
* Basic main view constructor injecting dependencies.
*
* @param someMessages
* A <i>Jmix</i> core messages component.
* @param aCurrentUserSubstitution
* A <i>Jmix</i> core current user substitution component.
* @param someUIComponents
* A <i>Jmix</i> (flow) UI components component.
* @throws NullPointerException
* Will be thrown, if any argument is {@code null}.
*/
protected BasicMainView(@NonNull final Messages someMessages,
@NonNull final CurrentUserSubstitution aCurrentUserSubstitution,
@NonNull final UiComponents someUIComponents)
{
super();
Objects.requireNonNull(someMessages);
Objects.requireNonNull(aCurrentUserSubstitution);
Objects.requireNonNull(someUIComponents);
messages = someMessages;
currentUserSubstitution = aCurrentUserSubstitution;
uiComponents = someUIComponents;
}
/**
* <p>Renders some user specific details onto the user menu button.</p>
* <p>This method is indented to be called by the application specific main view controller button renderer, e.g.:
* </p>
* <pre>
* {@literal @}Install(to = "userMenu", subject = "buttonRenderer")
* private Component userMenuButtonRenderer(final UserDetails someUserDetails) {
* return someUserDetails instanceof final User user ? createUserMenuButtonRenderer(user) : null;
* }
* </pre>
*
* @param aUser
* A user.
* @return button component or {@code null}
*/
protected Component createUserMenuButtonRenderer(@Nullable final U aUser)
{
final Component result;
if (aUser == null)
{
result = null;
}
else
{
final boolean substituted = isSubstituted(aUser);
final String name = generateName(aUser);
final Div content = uiComponents.create(Div.class);
content.setClassName("user-menu-button-content");
final Avatar avatar = createAvatar(name);
if (!substituted)
{
final String avatarUrl = getUserAvatarUrl(aUser);
if (avatarUrl != null)
{
avatar.setImage(avatarUrl);
}
}
final Span text = uiComponents.create(Span.class);
text.setText(name);
text.setClassName("user-menu-text");
content.add(avatar, text);
if (substituted)
{
final Span subtext = uiComponents.create(Span.class);
subtext.setText(messages.getMessage("userMenu.substituted"));
subtext.setClassName("user-menu-subtext");
content.add(subtext);
}
result = content;
}
return result;
}
/**
* <p>Renders some user specific details onto the user menu header.</p>
* <p>This method is indented to be called by the application specific main view controller button renderer, e.g.:
* </p>
* <pre>
* {@literal @}Install(to = "userMenu", subject = "headerRenderer")
* private Component userMenuHeaderRenderer(final UserDetails someUserDetails) {
* return someUserDetails instanceof final User user ? createUserMenuHeaderRenderer(user) : null;
* }
* </pre>
*
* @param aUser
* A user.
* @return header content component or {@code null}
*/
protected Component createUserMenuHeaderRenderer(@Nullable final U aUser)
{
final Component result;
if (aUser == null)
{
result = null;
}
else
{
final Div content = uiComponents.create(Div.class);
content.setClassName("user-menu-header-content");
final String name = generateName(aUser);
final boolean substituted = isSubstituted(aUser);
final Avatar avatar = createAvatar(name);
avatar.addThemeVariants(AvatarVariant.LUMO_LARGE);
if (!substituted)
{
final String avatarUrl = getUserAvatarUrl(aUser);
if (avatarUrl != null)
{
avatar.setImage(avatarUrl);
}
}
final Span text = uiComponents.create(Span.class);
text.setText(name);
text.setClassName("user-menu-text");
content.add(avatar, text);
if (Objects.equals(name, aUser.getUsername()))
{
text.addClassNames("user-menu-text-subtext");
}
else
{
final Span subtext = uiComponents.create(Span.class);
subtext.setText(aUser.getUsername());
subtext.setClassName("user-menu-subtext");
content.add(subtext);
}
result = content;
}
return result;
}
/**
* <p>Default implementation to return a custom user avatar URL for a given user or {@code null}.</p>
* <p>Overwrite this method to return a user specific avatar URL.</p>
*
* @param aUser
* A user.
* @return user avatar URL
*/
@Nullable
protected String getUserAvatarUrl(@Nullable final U aUser)
{
return null;
}
/**
* Creates an avatar with the given name.
*
* @param aName
* A name.
* @return avatar
*/
@NonNull
protected Avatar createAvatar(final String aName)
{
final Avatar avatar = uiComponents.create(Avatar.class);
avatar.setName(aName);
avatar.getElement().setAttribute("tabindex", "-1");
avatar.setClassName("user-menu-avatar");
return avatar;
}
/**
* <p>Default implementation to return a user's name.</p>
* <p>This method is intended to be overwritten by the application specific main view controller, e.g.:</p>
* <pre>
* {@literal @}Override
* protected String generateName(final User aUser) {
* String userName = String.format("%s %s",
* Strings.nullToEmpty(aUser.getFirstName()),
* Strings.nullToEmpty(aUser.getLastName()))
* .trim();
*
* return userName.isEmpty() ? super.generateName(aUser) : userName;
* }
* </pre>
*
* @param aUser
* A user.
* @return name
*/
@NonNull
protected String generateName(@Nullable final U aUser)
{
return aUser == null ? "" : aUser.getUsername();
}
/**
* Returns, whether the given user is substituted by comparing the {@code username} of the currently authenticated
* on with the given user's {@code username}.
*
* @param aUser
* A user.
* @return substituted
*/
protected boolean isSubstituted(@Nullable final U aUser)
{
final UserDetails authenticatedUser = currentUserSubstitution.getAuthenticatedUser();
return aUser != null && !Objects.equals(authenticatedUser.getUsername(), aUser.getUsername());
}
}