diff --git a/src/main/java/io/jenkins/plugins/customizable_header/AppNavLink.java b/src/main/java/io/jenkins/plugins/customizable_header/AppNavLink.java index a6d5580..7791b83 100644 --- a/src/main/java/io/jenkins/plugins/customizable_header/AppNavLink.java +++ b/src/main/java/io/jenkins/plugins/customizable_header/AppNavLink.java @@ -27,6 +27,7 @@ public class AppNavLink extends AbstractLink { private String url; private String label; private Logo logo; + private String id; private boolean external; @@ -83,6 +84,15 @@ public void setLogo(Logo logo) { this.logo = logo; } + @Exported + public String getId() { + return id; + } + + @DataBoundSetter + public void setId(String id) { + this.id = id; + } @Exported @Override diff --git a/src/main/java/io/jenkins/plugins/customizable_header/FavoriteIntegrationListener.java b/src/main/java/io/jenkins/plugins/customizable_header/FavoriteIntegrationListener.java new file mode 100644 index 0000000..8065a45 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/customizable_header/FavoriteIntegrationListener.java @@ -0,0 +1,117 @@ +package io.jenkins.plugins.customizable_header; + +import hudson.Extension; +import hudson.model.Item; +import hudson.model.User; +import hudson.plugins.favorite.Favorites; +import hudson.plugins.favorite.listener.FavoriteListener; +import jenkins.model.Jenkins; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest2; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +@Extension(optional = true) +public class FavoriteIntegrationListener extends FavoriteListener { + + private static final Logger LOGGER = Logger.getLogger(FavoriteIntegrationListener.class.getName()); + private static final String FAVORITES_ID = "favorites-nav"; + + @Override + public void onAddFavourite(Item item, User user) { + LOGGER.info("DEBUG: onAddFavourite triggered for item " + item.getFullName() + " and user " + user.getId()); + updateFavoritesNavLink(user, true); + // Trigger the favorite status refresh event for UI + triggerAppNavRefresh(); + } + + @Override + public void onRemoveFavourite(Item item, User user) { + LOGGER.info("DEBUG: onRemoveFavourite triggered for item " + item.getFullName() + " and user " + user.getId()); + // Check if the user has any favorites left + boolean hasFavorites = !isEmpty(Favorites.getFavorites(user)); + LOGGER.info("DEBUG: User " + user.getId() + " has favorites: " + hasFavorites); + updateFavoritesNavLink(user, hasFavorites); + // Trigger the favorite status refresh event for UI + triggerAppNavRefresh(); + } + + private void updateFavoritesNavLink(User user, boolean hasFavorites) { + try { + // Get user header + UserHeader headerProp = user.getProperty(UserHeader.class); + if (headerProp == null) { + return; + } + + List links = headerProp.getLinks(); + + // Find existing favorites link + AppNavLink favoritesLink = null; + for (AbstractLink link : links) { + if (link instanceof AppNavLink && FAVORITES_ID.equals(((AppNavLink)link).getId())) { + favoritesLink = (AppNavLink) link; + break; + } + } + + if (hasFavorites) { + // Add favorites link if it doesn't exist and there are no other links + if (favoritesLink == null && links.isEmpty()) { + AppNavLink newLink = createFavoritesLink(user); + List newLinks = new ArrayList<>(); + newLinks.add(newLink); + headerProp.setLinks(newLinks); + user.save(); + LOGGER.fine("Added favorites link for user: " + user.getId()); + } + } else { + // Remove favorites link if it exists and it's the only link + if (favoritesLink != null && links.size() == 1) { + headerProp.setLinks(new ArrayList<>()); + user.save(); + LOGGER.fine("Removed favorites link for user: " + user.getId()); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to update favorites link for user: " + user.getId(), e); + } + } + + /** + * Triggers a refresh of the app-nav button visibility in the UI + * Called after favorite status changes to update the UI in real-time + */ + private void triggerAppNavRefresh() { + try { + // Use Jenkins.getInstance() to get the Jenkins instance + Jenkins jenkins = Jenkins.get(); + if (jenkins != null) { + LOGGER.info("DEBUG: Triggering app-nav refresh after favorite change"); + // Notify the frontend about the favorite status change + jenkins.getExtensionList(HeaderRootAction.class) + .get(0) + .notifyFavoriteStatusChanged(); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to trigger app-nav refresh after favorite change", e); + } + } + + private AppNavLink createFavoritesLink(User user) { + // Create a new App Nav Link for favorites + AppNavLink link = new AppNavLink( + "user/" + user.getId() + "/favorites", + "Favorites", + new io.jenkins.plugins.customizable_header.logo.Symbol("symbol-star") + ); + link.setId(FAVORITES_ID); + return link; + } + + private boolean isEmpty(Iterable iterable) { + return !iterable.iterator().hasNext(); + } +} \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/customizable_header/HeaderRootAction.java b/src/main/java/io/jenkins/plugins/customizable_header/HeaderRootAction.java index d5322f4..c69af08 100644 --- a/src/main/java/io/jenkins/plugins/customizable_header/HeaderRootAction.java +++ b/src/main/java/io/jenkins/plugins/customizable_header/HeaderRootAction.java @@ -10,6 +10,8 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; @@ -28,6 +30,8 @@ */ @Extension public class HeaderRootAction implements UnprotectedRootAction { + private static final Logger LOGGER = Logger.getLogger(HeaderRootAction.class.getName()); + @Override public String getIconFileName() { return null; @@ -67,6 +71,45 @@ public boolean hasLinks() { return CustomHeaderConfiguration.get().hasLinks(); } + /** + * Notifies the frontend about favorite status changes + * This method is called by the FavoriteIntegrationListener when a favorite is added or removed + */ + public void notifyFavoriteStatusChanged() { + LOGGER.info("DEBUG: notifyFavoriteStatusChanged called in HeaderRootAction"); + // The actual notification is handled via the /favoriteStatusChanged endpoint + } + + /** + * Endpoint for the frontend to check if there was a favorite status change + * @return JSON response indicating whether the app-nav button should be shown/hidden + */ + @GET + public FavoriteStatus doFavoriteStatusChanged() { + + Jenkins.get().checkPermission(Jenkins.READ); + boolean hasLinks = hasLinks(); + LOGGER.info("DEBUG: doFavoriteStatusChanged endpoint called, hasLinks=" + hasLinks); + return new FavoriteStatus(hasLinks); + } + + /** + * Simple status object for favorite status changes + */ + @ExportedBean + public static class FavoriteStatus { + private final boolean showAppNav; + + public FavoriteStatus(boolean showAppNav) { + this.showAppNav = showAppNav; + } + + @Exported + public boolean isShowAppNav() { + return showAppNav; + } + } + @POST public HttpResponse doAddSystemMessage(@QueryParameter(fixEmpty = true) String message, @QueryParameter(fixEmpty = true) String level, @QueryParameter String expireDate, @QueryParameter(fixEmpty = true) String id, @QueryParameter(fixEmpty = true) Boolean dismissible) throws IOException { diff --git a/src/main/js/app-nav/index.js b/src/main/js/app-nav/index.js index 3e37bea..03b3ad4 100644 --- a/src/main/js/app-nav/index.js +++ b/src/main/js/app-nav/index.js @@ -93,6 +93,182 @@ function callback(element, instance) { .finally(() => (instance.loaded = true)); } +// Function to check app-nav visibility +function checkAppNavVisibility() { + const appNavButton = document.querySelector(".custom-header__app-nav-button"); + + console.log("DEBUG: checkAppNavVisibility called"); + + if (!appNavButton) { + console.log("DEBUG: appNavButton not found in DOM"); + return; // Button doesn't exist, nothing to do + } + + // Get the Jenkins root URL from the page context or calculate it + const rootURL = (window.rootURL || '') || + (document.querySelector('head base')?.getAttribute('href')?.replace(/\/$/, '') || ''); + + console.log("DEBUG: Fetching favorite status from:", `${rootURL}/customizable-header/favoriteStatusChanged`); + + // Fetch the current status from the endpoint + fetch(`${rootURL}/customizable-header/favoriteStatusChanged`) + .then((response) => response.json()) + .then((data) => { + console.log("DEBUG: Got favorite status response:", data); + if (data.showAppNav) { + appNavButton.style.display = ""; // Show the button + console.log("DEBUG: Showing app-nav button"); + } else { + appNavButton.style.display = "none"; // Hide the button + console.log("DEBUG: Hiding app-nav button"); + } + }) + .catch((error) => console.error("Error checking app-nav visibility:", error)); +} + +// Set up favorite plugin event listeners +function setupFavoriteEventListeners() { + console.log("DEBUG: Setting up favorite event listeners"); + + // Listen for favorite changes using a custom event + // This will be triggered by a MutationObserver watching for DOM changes from the favorite plugin + document.addEventListener("jenkins:favorite-changed", () => { + console.log("DEBUG: jenkins:favorite-changed event received"); + checkAppNavVisibility(); + }); + + // Try to find favorite icons with alternative selectors + function findAllFavoriteIcons() { + // Different Jenkins versions and themes might use different classes for favorite icons + const selectors = [ + ".icon-favorite", + ".icon-favorite-inactive", + ".favorite-icon", + ".fav-button", + "svg.icon-favorite", + "svg.icon-fav", + ".favorite-toggle", + "[data-favorite]", + "a.favorite" + ]; + + let allIcons = []; + selectors.forEach(selector => { + const icons = document.querySelectorAll(selector); + console.log(`DEBUG: Found ${icons.length} icons with selector "${selector}"`); + allIcons = [...allIcons, ...icons]; + }); + + return allIcons; + } + + // Directly add click handler to favorite icons + function addClickHandlers() { + const allFavoriteButtons = findAllFavoriteIcons(); + console.log("DEBUG: Adding click handlers to", allFavoriteButtons.length, "favorite buttons"); + + allFavoriteButtons.forEach(button => { + // Remove existing handlers first to avoid duplicates + button.removeEventListener("click", favoriteClickHandler); + button.addEventListener("click", favoriteClickHandler); + }); + } + + function favoriteClickHandler(event) { + console.log("DEBUG: Favorite icon clicked", event.target); + // Give the server a moment to process the favorite change + setTimeout(() => { + console.log("DEBUG: Dispatching favorite-changed event after click"); + document.dispatchEvent(new CustomEvent("jenkins:favorite-changed")); + }, 500); + } + + // Set up a more comprehensive MutationObserver + const observer = new MutationObserver((mutations) => { + console.log("DEBUG: MutationObserver triggered, mutations:", mutations.length); + + let favoriteChanged = false; + + mutations.forEach((mutation) => { + // Check if this is a class change on an element that might be a favorite button + if (mutation.type === "attributes" && mutation.attributeName === "class") { + const target = mutation.target; + const classList = target.classList ? Array.from(target.classList) : []; + + console.log("DEBUG: Class mutation on", target, "classList:", classList); + + // Check for any class that might indicate a favorite icon + if (classList.some(cls => cls.includes("fav") || cls.includes("favorite"))) { + console.log("DEBUG: Potential favorite icon class change detected"); + favoriteChanged = true; + } + } + + // Check for added/removed nodes that might be favorite icons + if (mutation.type === "childList") { + // Check added nodes + mutation.addedNodes.forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + const hasFavoriteClass = node.classList && + Array.from(node.classList).some(cls => cls.includes("fav") || cls.includes("favorite")); + + if (hasFavoriteClass) { + console.log("DEBUG: Favorite icon added to DOM", node); + favoriteChanged = true; + } + + // Check for favorite icons inside the added node + const favIcons = node.querySelectorAll('[class*="fav"], [class*="favorite"]'); + if (favIcons.length > 0) { + console.log("DEBUG: Found favorite icons inside added node", favIcons); + favoriteChanged = true; + } + } + }); + } + }); + + if (favoriteChanged) { + console.log("DEBUG: Favorite change detected, dispatching event"); + document.dispatchEvent(new CustomEvent("jenkins:favorite-changed")); + + // Re-add click handlers to any new favorite buttons + addClickHandlers(); + } + }); + + // Start observing for changes + window.setTimeout(() => { + // First, set up click handlers + addClickHandlers(); + + // Observe the entire document for any changes that might affect favorites + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["class"] + }); + + console.log("DEBUG: Comprehensive observer setup complete"); + + // Also add a click listener to the document to catch any favorite button clicks + document.addEventListener("click", function(event) { + // Check if the clicked element or any of its parents is a favorite icon + let target = event.target; + while (target && target !== document) { + if (target.classList && + Array.from(target.classList).some(cls => cls.includes("fav") || cls.includes("favorite"))) { + console.log("DEBUG: Favorite icon or container clicked via document listener", target); + setTimeout(() => checkAppNavVisibility(), 500); + break; + } + target = target.parentNode; + } + }); + }, 1000); // Short delay to ensure DOM is ready +} + function init() { Behaviour.specify(".custom-header__app-nav-button", "app-nav", 0, function (element) { tippy( @@ -126,6 +302,15 @@ function init() { } ) }); + + // Check app-nav visibility initially + checkAppNavVisibility(); + + // Set up favorite event listeners + setupFavoriteEventListeners(); + + // Periodically check for app-nav visibility (as a fallback) + window.setInterval(checkAppNavVisibility, 5000); } export default { init }; diff --git a/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/config.jelly b/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/config.jelly index 05ee468..84b7469 100644 --- a/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/config.jelly +++ b/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/config.jelly @@ -1,7 +1,7 @@ - + diff --git a/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/index.jelly b/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/index.jelly index 10b5e9c..ffb16d3 100644 --- a/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/index.jelly +++ b/src/main/resources/io/jenkins/plugins/customizable_header/SystemMessage/index.jelly @@ -1,7 +1,9 @@ - + -