diff --git a/libs/blocks/floodgateui/actions/index.js b/libs/blocks/floodgateui/actions/index.js index d2350e0f32..a14293b118 100644 --- a/libs/blocks/floodgateui/actions/index.js +++ b/libs/blocks/floodgateui/actions/index.js @@ -7,6 +7,7 @@ import { origin, preview } from '../../locui/utils/franklin.js'; import { decorateSections } from '../../../utils/utils.js'; import { getUrls } from '../../locui/loc/index.js'; import { validateUrlsFormat } from '../floodgate/index.js'; +import { isUrl } from '../utils/url.js'; export const showRolloutOptions = signal(false); @@ -92,7 +93,7 @@ async function findPageFragments(path) { if (accDupe || dupe) return acc; const fragmentUrl = new URL(`${origin}${pathname}`); - fragmentUrl.alt = fragment.textContent; + fragmentUrl.alt = isUrl(fragment.textContent) ? fragment.textContent : originalUrl; acc.push(fragmentUrl); return acc; }, []); @@ -119,7 +120,7 @@ async function findDeepFragments(path) { searched.push(search.pathname); } } - return fragments.length ? fragments : []; + return fragments.length ? getUrls(fragments) : []; } export async function findFragments() { diff --git a/libs/blocks/floodgateui/floodgate/index.js b/libs/blocks/floodgateui/floodgate/index.js index 39eaf1a612..4e9dd8d4dd 100644 --- a/libs/blocks/floodgateui/floodgate/index.js +++ b/libs/blocks/floodgateui/floodgate/index.js @@ -23,18 +23,28 @@ const repo = urlParams.get('repo') || 'milo'; let resourcePath; let previewPath; +export function validateOrigin(urlStr) { + try { + const url = new URL(urlStr); + const origins = [url.origin.replace('.aem.', '.hlx.'), url.origin.replace('.hlx.', '.aem.')]; + return origins.includes(origin); + } catch { + return false; + } +} + export function validateUrlsFormat(projectUrls, removeMedia = false) { projectUrls.forEach((projectUrl, idx) => { const urlObj = getUrl(projectUrl); const url = isUrl(urlObj.alt) ?? urlObj; - if (url.origin !== origin) { + if (!validateOrigin(url.origin)) { const aemUrl = url.hostname?.split('--').length === 3; url.valid = !aemUrl ? 'not AEM url' : 'not same domain'; } if ((/\.(gif|jpg|jpeg|tiff|png|webp)$/i).test(url.pathname)) { url.valid = 'media'; } - projectUrls[idx] = Array.isArray(projectUrls[idx]) ? [url] : url; + projectUrls[idx] = Array.isArray(projectUrls[idx]) ? [urlObj] : urlObj; }); if (removeMedia) { return projectUrls.filter((url) => getUrl(url).valid !== 'media'); diff --git a/libs/blocks/floodgateui/utils/miloc.js b/libs/blocks/floodgateui/utils/miloc.js index bda9347635..1eff940680 100644 --- a/libs/blocks/floodgateui/utils/miloc.js +++ b/libs/blocks/floodgateui/utils/miloc.js @@ -120,6 +120,7 @@ export async function getServiceConfigFg(origin) { export async function fetchStatusAction() { // fetch copy status const config = await getServiceConfigFg(origin); + if (!config || !heading.value.env) return{}; const paramsFg = await getParamsFg(config); const excelPath = paramsFg.projectExcelPath; const env = heading.value.env; diff --git a/libs/blocks/locui/actions/index.js b/libs/blocks/locui/actions/index.js index f66a5190d7..82163ebce3 100644 --- a/libs/blocks/locui/actions/index.js +++ b/libs/blocks/locui/actions/index.js @@ -1,6 +1,8 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ -import { heading, urls, languages, allowSyncToLangstore, allowSendForLoc, allowRollout } from '../utils/state.js'; +import { + heading, urls, languages, allowSyncToLangstore, allowSendForLoc, allowRollout, +} from '../utils/state.js'; import { setExcelStatus, setStatus } from '../utils/status.js'; import { origin, preview } from '../utils/franklin.js'; import { decorateSections } from '../../../utils/utils.js'; diff --git a/libs/blocks/locui/utils/franklin.js b/libs/blocks/locui/utils/franklin.js index 75964c7f8d..e30a877905 100644 --- a/libs/blocks/locui/utils/franklin.js +++ b/libs/blocks/locui/utils/franklin.js @@ -1,21 +1,36 @@ -const ADMIN = 'https://admin.hlx.page'; +import { SLD } from '../../../utils/utils.js'; +const ADMIN = 'https://admin.hlx.page'; const urlParams = new URLSearchParams(window.location.search); const owner = urlParams.get('owner') || 'adobecom'; const repo = urlParams.get('repo') || 'milo'; -export const origin = `https://main--${repo}--${owner}.hlx.page`; +export const origin = `https://main--${repo}--${owner}.${SLD}.page`; + +// Temporary fix until https://github.com/adobe/helix-admin/issues/2831 is fixed. +function fixPreviewDomain(json) { + if (SLD === 'aem') { + if (json?.preview?.url) { + json.preview.url = json.preview.url.replace('.hlx.', '.aem.'); + } + if (json?.live?.url) { + json.live.url = json.live.url.replace('.hlx.', '.aem.'); + } + } +} export async function preview(path) { const url = `${ADMIN}/preview/${owner}/${repo}/main${path}`; const resp = await fetch(url, { method: 'POST' }); const json = await resp.json(); + fixPreviewDomain(json); return json; } -export async function getStatus(path = '', editUrl = 'auto', fgFlag = false, fgColor = null) { - let url = `${ADMIN}/status/${owner}/${fgFlag ? `${repo}-${fgColor}` : repo}/main${path}`; +export async function getStatus(path = '', editUrl = 'auto') { + let url = `${ADMIN}/status/${owner}/${repo}/main${path}`; url = editUrl ? `${url}?editUrl=${editUrl}` : url; const resp = await fetch(url, { cache: 'reload' }); const json = await resp.json(); + fixPreviewDomain(json); return json; } diff --git a/libs/tools/sharepoint/shared.js b/libs/tools/sharepoint/shared.js index 82093bcea2..cdaf9b159f 100644 --- a/libs/tools/sharepoint/shared.js +++ b/libs/tools/sharepoint/shared.js @@ -1,4 +1,5 @@ import getServiceConfig from '../../utils/service-config.js'; +import { SLD } from '../../utils/utils.js'; const MOCK_REFERRER = 'https://adobe.sharepoint.com/:x:/r/sites/adobecom/_layouts/15/Doc.aspx?sourcedoc=%7B654BFAD2-84A7-442D-A13D-18DE87A405B7%7D'; @@ -14,7 +15,7 @@ export async function getSharePointDetails(hlxOrigin) { } export function getItemId() { - const referrer = new URLSearchParams(window.location.search).get('referrer') || MOCK_REFERRER; + const referrer = new URLSearchParams(window.location.search).get('referrer'); const sourceDoc = referrer?.match(/sourcedoc=([^&]+)/)[1]; const sourceId = decodeURIComponent(sourceDoc); return sourceId.slice(1, -1); @@ -24,5 +25,5 @@ export function getSiteOrigin() { const search = new URLSearchParams(window.location.search); const repo = search.get('repo'); const owner = search.get('owner'); - return repo && owner ? `https://main--${repo}--${owner}.hlx.live` : window.location.origin; + return repo && owner ? `https://main--${repo}--${owner}.${SLD}.page` : window.location.origin; } diff --git a/libs/utils/sidekick.js b/libs/utils/sidekick.js index 5edfe39fa3..237d714583 100644 --- a/libs/utils/sidekick.js +++ b/libs/utils/sidekick.js @@ -1,13 +1,69 @@ +function stylePublish(sk) { + const pubPlg = sk.shadowRoot.querySelector('.publish.plugin'); + if (!pubPlg) return; + const style = document.createElement('style'); + const span = document.createElement('span'); + span.textContent = 'Are you sure? This will publish to production.'; + const btn = pubPlg.querySelector('button'); + const publishStyles = ` + .plugin.update { + --bg-color: rgb(129 27 14); + --text-color: #fff0f0; + + color-scheme: light dark; + display: flex; + order: 100; + } + .publish.plugin > button { + background: var(--bg-color); + border-color: #b46157; + color: var(--text-color); + } + .publish.plugin > button > span { + display: none; + background: var(--bg-color); + border-radius: 4px; + line-height: 1.2rem; + padding: 8px 12px; + position: absolute; + top: 34px; + left: 50%; + transform: translateX(-50%); + width: 150px; + white-space: pre-wrap; + } + .publish.plugin > button:hover > span { + display: block; + color: var(--text-color); + } + .publish.plugin > button > span:before { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid var(--bg-color); + position: absolute; + text-align: center; + top: -6px; + left: 50%; + transform: translateX(-50%); + } + `; + style.append(publishStyles); + pubPlg.prepend(style); + btn.append(span); +} + // loadScript and loadStyle are passed in to avoid circular dependencies export default function init({ createTag, loadBlock, loadScript, loadStyle }) { // manifest v3 const sendToCaasListener = async (e) => { const { host, project, ref: branch, repo, owner } = e.detail.data.config; + // eslint-disable-next-line import/no-unresolved const { sendToCaaS } = await import('https://milo.adobe.com/tools/send-to-caas/send-to-caas.js'); sendToCaaS({ host, project, branch, repo, owner }, loadScript, loadStyle); }; - const checkSchemaListener = async (e) => { + const checkSchemaListener = async () => { const schema = document.querySelector('script[type="application/ld+json"]'); if (schema === null) return; const resultsUrl = 'https://search.google.com/test/rich-results?url='; @@ -29,10 +85,44 @@ export default function init({ createTag, loadBlock, loadScript, loadStyle }) { sendToCaaS({ host, project, branch, repo, owner }, loadScript, loadStyle); }); - const sk = document.querySelector('helix-sidekick'); + const sk = document.querySelector('aem-sidekick, helix-sidekick'); // Add plugin listeners here sk.addEventListener('custom:send-to-caas', sendToCaasListener); sk.addEventListener('custom:check-schema', checkSchemaListener); sk.addEventListener('custom:preflight', preflightListener); + + // Color code publish button + stylePublish(sk); } + +function onSkLoaded(callback) { + // sidekick nextGen + const observer = new MutationObserver(() => { + const sidekick = document.querySelector('aem-sidekick'); + if (sidekick) { + observer.disconnect(); + callback(sidekick); + } + }); + observer.observe(document.body, { childList: true, subtree: true }); + + // sidekick v1 ready event + document.addEventListener('sidekick-ready', () => { + callback(document.querySelector('helix-sidekick')); + }, { once: true }); +} + +export function connectSK(status, standby = null) { + const sidekick = document.querySelector('aem-sidekick, helix-sidekick'); + if (sidekick) { + sidekick.addEventListener('statusfetched', status); + sidekick.addEventListener('status-fetched', status); + } else { + standby?.(); + onSkLoaded((sk) => { + sk?.addEventListener('statusfetched', status); + sk?.addEventListener('status-fetched', status); + }); + } +} \ No newline at end of file diff --git a/libs/utils/utils.js b/libs/utils/utils.js index 0cb47625e0..4968f6d8c3 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -134,6 +134,7 @@ export const MILO_EVENTS = { DEFERRED: 'milo:deferred' }; const LANGSTORE = 'langstore'; const PAGE_URL = new URL(window.location.href); +export const SLD = PAGE_URL.hostname.includes('.aem.') ? 'aem' : 'hlx'; function getEnv(conf) { const { host } = window.location; @@ -142,10 +143,11 @@ function getEnv(conf) { if (query) return { ...ENVS[query], consumer: conf[query] }; if (host.includes('localhost')) return { ...ENVS.local, consumer: conf.local }; /* c8 ignore start */ - if (host.includes('hlx.page') - || host.includes('hlx.live') - || host.includes('stage.adobe') - || host.includes('corp.adobe')) { + if (host.includes(`${SLD}.page`) + || host.includes(`${SLD}.live`) + || host.includes('stage.adobe') + || host.includes('corp.adobe') + || host.includes('graybox.adobe')) { return { ...ENVS.stage, consumer: conf.stage }; } return { ...ENVS.prod, consumer: conf.prod }; @@ -229,6 +231,7 @@ export const [setConfig, updateConfig, getConfig] = (() => { config.useDotHtml = !PAGE_URL.origin.includes('.hlx.') && (conf.useDotHtml ?? PAGE_URL.pathname.endsWith('.html')); config.entitlements = handleEntitlements; + config.consumerEntitlements = conf.entitlements || []; setupMiloObj(config); return config; }, @@ -241,7 +244,7 @@ export function isInTextNode(node) { return node.parentElement.firstChild.nodeType === Node.TEXT_NODE; } -export function createTag(tag, attributes, html) { +export function createTag(tag, attributes, html, options = {}) { const el = document.createElement(tag); if (html) { if (html instanceof HTMLElement @@ -259,6 +262,7 @@ export function createTag(tag, attributes, html) { el.setAttribute(key, val); }); } + options.parent?.append(el); return el; } @@ -429,31 +433,19 @@ export async function loadTemplate() { await Promise.all([styleLoaded, scriptLoaded]); } -function checkForExpBlock(name, expBlocks) { - const expBlock = expBlocks?.[name]; - if (!expBlock) return null; - - const blockName = expBlock.split('/').pop(); - return { blockPath: expBlock, blockName }; -} - export async function loadBlock(block) { if (block.classList.contains('hide-block')) { block.remove(); return null; } - let name = block.classList[0]; + const name = block.classList[0]; const { miloLibs, codeRoot, expBlocks } = getConfig(); const base = miloLibs && MILO_BLOCKS.includes(name) ? miloLibs : codeRoot; let path = `${base}/blocks/${name}`; - const expBlock = checkForExpBlock(name, expBlocks); - if (expBlock) { - name = expBlock.blockName; - path = expBlock.blockPath; - } + if (expBlocks?.[name]) path = expBlocks[name]; const blockPath = `${path}/${name}`; @@ -521,16 +513,22 @@ export function decorateImageLinks(el) { const [source, alt, icon] = img.alt.split('|'); try { const url = new URL(source.trim()); - const href = url.hostname.includes('.hlx.') ? `${url.pathname}${url.hash}` : url.href; + const href = url.hostname.includes('.hlx.') ? url.pathname : url.href; if (alt?.trim().length) img.alt = alt.trim(); const pic = img.closest('picture'); const picParent = pic.parentElement; - const aTag = createTag('a', { href, class: 'image-link' }); - picParent.insertBefore(aTag, pic); - if (icon) { - import('./image-video-link.js').then((mod) => mod.default(picParent, aTag, icon)); + if (href.includes('.mp4')) { + const a = createTag('a', { href: url, 'data-video-poster': img.src }); + a.innerHTML = url; + pic.replaceWith(a); } else { - aTag.append(pic); + const aTag = createTag('a', { href, class: 'image-link' }); + picParent.insertBefore(aTag, pic); + if (icon) { + import('./image-video-link.js').then((mod) => mod.default(picParent, aTag, icon)); + } else { + aTag.append(pic); + } } } catch (e) { console.log('Error:', `${e.message} '${source.trim()}'`); @@ -563,7 +561,9 @@ export function decorateAutoBlock(a) { return false; } - if (key === 'fragment') { + const hasExtension = a.href.split('/').pop().includes('.'); + const mp4Match = a.textContent.match('media_.*.mp4'); + if (key === 'fragment' && (!hasExtension || mp4Match)) { if (a.href === window.location.href) { return false; } @@ -580,7 +580,7 @@ export function decorateAutoBlock(a) { } // previewing a fragment page with mp4 video - if (a.textContent.match('media_.*.mp4')) { + if (mp4Match) { a.className = 'video link-block'; return false; } @@ -670,8 +670,7 @@ function decorateHeader() { header.remove(); return; } - const headerQuery = new URLSearchParams(window.location.search).get('headerqa'); - header.className = headerQuery || headerMeta || 'gnav'; + header.className = headerMeta || 'gnav'; const metadataConfig = getMetadata('breadcrumbs')?.toLowerCase() || getConfig().breadcrumbs; if (metadataConfig === 'off') return; @@ -711,11 +710,28 @@ async function loadFooter() { footer.remove(); return; } - const footerQuery = new URLSearchParams(window.location.search).get('footerqa'); - footer.className = footerQuery || footerMeta || 'footer'; + footer.className = footerMeta || 'footer'; await loadBlock(footer); } +export function filterDuplicatedLinkBlocks(blocks) { + if (!blocks?.length) return []; + const uniqueModalKeys = new Set(); + const uniqueBlocks = []; + for (const obj of blocks) { + if (obj.className.includes('modal')) { + const key = `${obj.dataset.modalHash}-${obj.dataset.modalPath}`; + if (!uniqueModalKeys.has(key)) { + uniqueModalKeys.add(key); + uniqueBlocks.push(obj); + } + } else { + uniqueBlocks.push(obj); + } + } + return uniqueBlocks; +} + function decorateSection(section, idx) { let links = decorateLinks(section); decorateDefaults(section); @@ -751,7 +767,7 @@ function decorateSection(section, idx) { blocks: [...links, ...blocks], el: section, idx, - preloadLinks: blockLinks.autoBlocks, + preloadLinks: filterDuplicatedLinkBlocks(blockLinks.autoBlocks), }; } @@ -760,28 +776,32 @@ export function decorateSections(el, isDoc) { return [...el.querySelectorAll(selector)].map(decorateSection); } -export async function decorateFooterPromo() { - const footerPromoTag = getMetadata('footer-promo-tag'); - const footerPromoType = getMetadata('footer-promo-type'); +export async function decorateFooterPromo(doc = document) { + const footerPromoTag = getMetadata('footer-promo-tag', doc); + const footerPromoType = getMetadata('footer-promo-type', doc); if (!footerPromoTag && footerPromoType !== 'taxonomy') return; const { default: initFooterPromo } = await import('../features/footer-promo.js'); - await initFooterPromo(footerPromoTag, footerPromoType); + await initFooterPromo(footerPromoTag, footerPromoType, doc); } let imsLoaded; export async function loadIms() { imsLoaded = imsLoaded || new Promise((resolve, reject) => { - const { locale, imsClientId, imsScope, env } = getConfig(); + const { locale, imsClientId, imsScope, env, base } = getConfig(); if (!imsClientId) { reject(new Error('Missing IMS Client ID')); return; } + const [unavMeta, ahomeMeta] = [getMetadata('universal-nav')?.trim(), getMetadata('adobe-home-redirect')]; + const defaultScope = `AdobeID,openid,gnav${unavMeta && unavMeta !== 'off' ? ',pps.read,firefly_api,additional_info.roles,read_organizations' : ''}`; const timeout = setTimeout(() => reject(new Error('IMS timeout')), 5000); window.adobeid = { client_id: imsClientId, - scope: imsScope || 'AdobeID,openid,gnav', + scope: imsScope || defaultScope, locale: locale?.ietf?.replace('-', '_') || 'en_US', + redirect_uri: ahomeMeta === 'on' + ? `https://www${env.name !== 'prod' ? '.stage' : ''}.adobe.com${locale.prefix}` : undefined, autoValidateToken: true, environment: env.ims, useLocalStorage: false, @@ -791,7 +811,10 @@ export async function loadIms() { }, onError: reject, }; - loadScript('https://auth.services.adobe.com/imslib/imslib.min.js'); + const path = PAGE_URL.searchParams.get('useAlternateImsDomain') + ? 'https://auth.services.adobe.com/imslib/imslib.min.js' + : `${base}/deps/imslib.min.js`; + loadScript(path); }).then(() => { if (!window.adobeIMS?.isSignedInUser()) { getConfig().entitlements([]); @@ -801,7 +824,7 @@ export async function loadIms() { return imsLoaded; } -async function loadMartech({ persEnabled = false, persManifests = [] } = {}) { +export async function loadMartech({ persEnabled = false, persManifests = [] } = {}) { // eslint-disable-next-line no-underscore-dangle if (window.marketingtech?.adobe?.launch && window._satellite) { return true; @@ -822,14 +845,15 @@ async function loadMartech({ persEnabled = false, persManifests = [] } = {}) { } async function checkForPageMods() { + const offFlag = (val) => PAGE_URL.searchParams.get(val) === 'off'; + if (offFlag('mep')) return; const persMd = getMetadata('personalization'); const promoMd = getMetadata('manifestnames'); const targetMd = getMetadata('target'); let persManifests = []; - const search = new URLSearchParams(window.location.search); - const persEnabled = persMd && persMd !== 'off' && search.get('personalization') !== 'off'; - const targetEnabled = targetMd && targetMd !== 'off' && search.get('target') !== 'off'; - const promoEnabled = promoMd && promoMd !== 'off'; + const persEnabled = persMd && persMd !== 'off' && !offFlag('personalization'); + const targetEnabled = targetMd && targetMd !== 'off' && !offFlag('target') && !offFlag('martech'); + const promoEnabled = promoMd && promoMd !== 'off' && !offFlag('promo'); const mepEnabled = persEnabled || targetEnabled || promoEnabled; if (mepEnabled) { @@ -849,14 +873,14 @@ async function checkForPageMods() { if (promoEnabled) { const { default: getPromoManifests } = await import('../features/personalization/promo-utils.js'); - persManifests = persManifests.concat(getPromoManifests(promoMd)); + persManifests = persManifests.concat(getPromoManifests(promoMd, PAGE_URL.searchParams)); } const { env } = getConfig(); let previewOn = false; const mep = PAGE_URL.searchParams.get('mep'); if (mep !== null || (env?.name !== 'prod' && mepEnabled)) { - previewOn = true; + previewOn = !offFlag('mepButton'); const { default: addPreviewToConfig } = await import('../features/personalization/add-preview-to-config.js'); persManifests = await addPreviewToConfig({ pageUrl: PAGE_URL, @@ -878,8 +902,6 @@ async function checkForPageMods() { const manifests = preloadManifests({ persManifests }, { getConfig, loadLink }); await applyPers(manifests); - } else { - document.body.dataset.mep = 'nopzn|nopzn'; } if (previewOn) { @@ -889,6 +911,11 @@ async function checkForPageMods() { } async function loadPostLCP(config) { + const georouting = getMetadata('georouting') || config.geoRouting; + if (georouting === 'on') { + const { default: loadGeoRouting } = await import('../features/georoutingv2/georoutingv2.js'); + await loadGeoRouting(config, createTag, getMetadata, loadBlock, loadStyle); + } loadMartech(); const header = document.querySelector('header'); if (header) { @@ -998,13 +1025,6 @@ function decorateDocumentExtras() { } async function documentPostSectionLoading(config) { - const georouting = getMetadata('georouting') || config.geoRouting; - if (georouting === 'on') { - // eslint-disable-next-line import/no-cycle - const { default: loadGeoRouting } = await import('../features/georoutingv2/georoutingv2.js'); - loadGeoRouting(config, createTag, getMetadata, loadBlock, loadStyle); - } - decorateFooterPromo(); const appendage = getMetadata('title-append'); @@ -1035,6 +1055,8 @@ async function documentPostSectionLoading(config) { import('../martech/attributes.js').then((analytics) => { document.querySelectorAll('main > div').forEach((section, idx) => analytics.decorateSectionAnalytics(section, idx, config)); }); + + document.body.appendChild(createTag('div', { id: 'page-load-ok-milo', style: 'display: none;' })); } async function processSection(section, config, isDoc) { @@ -1043,6 +1065,7 @@ async function processSection(section, config, isDoc) { const { default: loadInlineFrags } = await import('../blocks/fragment/fragment.js'); const fragPromises = inlineFrags.map((link) => loadInlineFrags(link)); await Promise.all(fragPromises); + await decoratePlaceholders(section.el, config); const newlyDecoratedSection = decorateSection(section.el, section.idx); section.blocks = newlyDecoratedSection.blocks; section.preloadLinks = newlyDecoratedSection.preloadLinks; @@ -1060,12 +1083,13 @@ async function processSection(section, config, isDoc) { // Only move on to the next section when all blocks are loaded. await Promise.all(loaded); + // Show the section when all blocks inside are done. + delete section.el.dataset.status; + if (isDoc && section.el.dataset.idx === '0') { - loadPostLCP(config); + await loadPostLCP(config); } - // Show the section when all blocks inside are done. - delete section.el.dataset.status; delete section.el.dataset.idx; return section.blocks; @@ -1158,3 +1182,5 @@ export function loadLana(options = {}) { window.addEventListener('error', lanaError); window.addEventListener('unhandledrejection', lanaError); } + +export const reloadPage = () => window.location.reload();