diff --git a/app/controllers/admin/navigation_links_controller.rb b/app/controllers/admin/navigation_links_controller.rb index 598590dae..4ba6c9cdf 100644 --- a/app/controllers/admin/navigation_links_controller.rb +++ b/app/controllers/admin/navigation_links_controller.rb @@ -1,6 +1,8 @@ module Admin class NavigationLinksController < Admin::ApplicationController after_action :bust_content_change_caches, only: %i[create update destroy] + after_action :bust_navigation_links_cache, only: %i[create update destroy] + ALLOWED_PARAMS = %i[ name url icon display_to position section ].freeze @@ -52,5 +54,10 @@ module Admin def navigation_link_params params.require(:navigation_link).permit(ALLOWED_PARAMS) end + + def bust_navigation_links_cache + Rails.cache.delete("navigation_links") + EdgeCache::Bust.call("/async_info/navigation_links") + end end end diff --git a/app/controllers/async_info_controller.rb b/app/controllers/async_info_controller.rb index 26c053c97..4a4065e32 100644 --- a/app/controllers/async_info_controller.rb +++ b/app/controllers/async_info_controller.rb @@ -1,6 +1,7 @@ # @note No pundit policy. All actions are unrestricted. class AsyncInfoController < ApplicationController NUMBER_OF_MINUTES_FOR_CACHE_EXPIRY = 15 + before_action :set_cache_control_headers, only: %i[navigation_links] def base_data flash.discard(:notice) @@ -61,4 +62,10 @@ class AsyncInfoController < ApplicationController #{current_user&.articles_count}__ #{current_user&.blocking_others_count}__" end + + def navigation_links + # We're sending HTML over the wire hence 'render layout: false' enforces rails NOT TO look for a layout file to wrap + # the view file - it allows us to not include the HTML headers for sending back to client. + render layout: false + end end diff --git a/app/javascript/packs/base.jsx b/app/javascript/packs/base.jsx index 4120973b8..bf74f1670 100644 --- a/app/javascript/packs/base.jsx +++ b/app/javascript/packs/base.jsx @@ -9,6 +9,14 @@ import { trackCreateAccountClicks } from '@utilities/ahoy/trackEvents'; import { showWindowModal, closeWindowModal } from '@utilities/showModal'; import * as Runtime from '@utilities/runtime'; +Document.prototype.ready = new Promise((resolve) => { + if (document.readyState !== 'loading') { + return resolve(); + } + document.addEventListener('DOMContentLoaded', () => resolve()); + return null; +}); + // Namespace for functions which need to be accessed in plain JS initializers window.Forem = { preactImport: undefined, @@ -62,17 +70,17 @@ function getPageEntries() { }); } +/** + * Initializes the left hand side hamburger menu + */ function initializeNav() { const { currentPage } = document.getElementById('page-content').dataset; const menuTriggers = [ - ...document.querySelectorAll( - '.js-hamburger-trigger, .hamburger a:not(.js-nav-more-trigger)', - ), + ...document.querySelectorAll('.js-hamburger-trigger, .hamburger a'), ]; - const moreMenus = [...document.getElementsByClassName('js-nav-more-trigger')]; setCurrentPageIconLink(currentPage, getPageEntries()); - initializeMobileMenu(menuTriggers, moreMenus); + initializeMobileMenu(menuTriggers); } const memberMenu = document.getElementById('crayons-header__menu'); @@ -82,6 +90,25 @@ if (memberMenu) { initializeMemberMenu(memberMenu, menuNavButton); } +/** + * Fetches the html for the navigation_links from an endpoint and dynamically insterts it in the DOM. + */ +async function getNavigation() { + const placeholderElement = document.getElementsByClassName( + 'js-navigation-links-container', + )[0]; + + if (placeholderElement.innerHTML.trim() === '') { + const response = await window.fetch(`/async_info/navigation_links`); + const htmlContent = await response.text(); + + const generatedElement = document.createElement('div'); + generatedElement.innerHTML = htmlContent; + + placeholderElement.appendChild(generatedElement); + } +} + // Initialize when asset pipeline (sprockets) initializers have executed waitOnBaseData() .then(() => { @@ -103,6 +130,7 @@ waitOnBaseData() Honeybadger.notify(error); }); +// we need to call initializeNav here for the initial page load initializeNav(); async function loadCreatorSettings() { @@ -125,6 +153,13 @@ if (document.location.pathname === '/admin/creator_settings/new') { loadCreatorSettings(); } +document.ready.then(() => { + const hamburgerTrigger = document.getElementsByClassName( + 'js-hamburger-trigger', + )[0]; + hamburgerTrigger.addEventListener('click', getNavigation); +}); + trackCreateAccountClicks('authentication-hamburger-actions'); trackCreateAccountClicks('authentication-top-nav-actions'); trackCreateAccountClicks('comments-locked-cta'); diff --git a/app/javascript/topNavigation/utilities.js b/app/javascript/topNavigation/utilities.js index f21dc48f8..b6ac2e6be 100644 --- a/app/javascript/topNavigation/utilities.js +++ b/app/javascript/topNavigation/utilities.js @@ -42,7 +42,7 @@ export function isTouchDevice() { /** * Initializes the member navigation menu events. * - * @param {HTMLElement} memberTopMenu The member menu in the top navigation. + * @param {HTMLElement} memberTopMenu The member menu in the top right navigation. * @param {HTMLElement} menuNavButton The button to activate the member navigation menu. */ export function initializeMemberMenu(memberTopMenu, menuNavButton) { @@ -122,11 +122,6 @@ function toggleBurgerMenu() { leftNavState === 'open' ? 'closed' : 'open'; } -function showMoreMenu({ target }) { - target.nextElementSibling.classList.remove('hidden'); - target.classList.add('hidden'); -} - /** * Gets a reference to InstantClick * @@ -155,17 +150,13 @@ export async function getInstantClick(waitTime = 2000) { /** * Initializes the hamburger menu for mobile navigation * - * @param {HTMLElement[]} menuTriggers - * @param {HTMLElement[]} moreMenus + * @param {HTMLElement[]} menuTriggers The menuTriggers include the hamburger to open the menu, + * the close icon to close the menu and the overlay to close the menu. */ -export function initializeMobileMenu(menuTriggers, moreMenus) { +export function initializeMobileMenu(menuTriggers) { menuTriggers.forEach((trigger) => { trigger.onclick = toggleBurgerMenu; }); - - moreMenus.forEach((trigger) => { - trigger.onclick = showMoreMenu; - }); } /** diff --git a/app/views/async_info/navigation_links.html.erb b/app/views/async_info/navigation_links.html.erb new file mode 100644 index 000000000..d4ab99326 --- /dev/null +++ b/app/views/async_info/navigation_links.html.erb @@ -0,0 +1,2 @@ +<%= render partial: "shared/auth_widget", locals: { tracking_id: "ca_hamburger_home_page", source: "mobile_hamburger_nav" } unless user_signed_in? %> +<%= render partial: "layouts/main_nav", locals: { context: "hamburger" } %> diff --git a/app/views/layouts/_main_nav.html.erb b/app/views/layouts/_main_nav.html.erb index d18345efb..a59f45dfd 100644 --- a/app/views/layouts/_main_nav.html.erb +++ b/app/views/layouts/_main_nav.html.erb @@ -1,26 +1,26 @@ -<% default_nav_links = NavigationLink.default_section.ordered.to_a %> -<% other_nav_links = NavigationLink.other_section.ordered.to_a %> +<% navigation_links = Rails.cache.fetch("navigation_links", expires_in: 15.minutes) do +{ + default_nav_ids: NavigationLink.default_section.ordered.ids, + other_nav_ids: NavigationLink.other_section.ordered.ids, + } +end %> <%# Reading Note: There's a faulty assumption that all "other_nav_links" are visible based on the current state (see ApplicationHelper#display_navigation_link? for details). %> -<% if other_nav_links.any? %> +<% if navigation_links[:other_nav_ids].any? %> <% end %> diff --git a/app/views/shared/_hamburger.html.erb b/app/views/shared/_hamburger.html.erb index 196d1da17..a66d38e6d 100644 --- a/app/views/shared/_hamburger.html.erb +++ b/app/views/shared/_hamburger.html.erb @@ -8,9 +8,7 @@ -
- <%= render partial: "shared/auth_widget", locals: { tracking_id: "ca_hamburger_home_page", source: "mobile_hamburger_nav" } unless user_signed_in? %> - <%= render partial: "layouts/main_nav", locals: { context: "hamburger" } %> +
diff --git a/config/routes.rb b/config/routes.rb index 060d91347..25030bc93 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -186,6 +186,7 @@ Rails.application.routes.draw do get "/social_previews/article/:id", to: "social_previews#article", as: :article_social_preview get "/async_info/base_data", controller: "async_info#base_data", defaults: { format: :json } + get "/async_info/navigation_links", controller: "async_info#navigation_links" # Settings post "users/join_org", to: "users#join_org" diff --git a/spec/requests/async_info_spec.rb b/spec/requests/async_info_spec.rb index af9d69a3c..7ec235968 100644 --- a/spec/requests/async_info_spec.rb +++ b/spec/requests/async_info_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe "AsyncInfo", type: :request do +RSpec.describe "AsyncInfo" do let(:controller_instance) { AsyncInfoController.new } before do @@ -30,4 +30,30 @@ RSpec.describe "AsyncInfo", type: :request do end end end + + describe "GET /async_info/navigation_links" do + it "returns html" do + get "/async_info/navigation_links" + expect(response).to have_http_status(:ok) + expect(response.body).to include("