/** * WordPress dependencies */ import apiFetch from '@safe-wordpress/api-fetch'; import { select, subscribe } from '@safe-wordpress/data'; import { render } from '@safe-wordpress/element'; import { _x } from '@safe-wordpress/i18n'; import domReady from '@safe-wordpress/dom-ready'; /** * External dependencies */ import { debounce, keys, toPairs } from 'lodash'; import { registerCommands } from '@nab/commands'; import { PreviewUrlButton, QuotaMeter, TestCreationButtons, TitleAction, } from '@nab/components'; import { store as NAB_DATA } from '@nab/data'; import { registerCoreExperiments } from '@nab/experiment-library'; import { store as NAB_EXPERIMENTS } from '@nab/experiments'; import { areEqual, createStagingNotice, logError, onElementReadyOrDomReadyTops, } from '@nab/utils'; import { formatNumber } from '@nab/i18n'; import type { CloudAlternativeResults, CloudResults, ExperimentTypeName, Maybe, Quota, SubscriptionPlan, } from '@nab/types'; /** * Internal dependencies */ import './style.scss'; import { renderHelp } from './help'; import { NoticeList } from './components/notice-list'; import DefaultIcon from '../../../images/icon.svg'; type Settings = { readonly staging: 'environment-type' | 'url' | false; readonly subscription: SubscriptionPlan | false; }; export function initPage( _: string, settings: Settings ): void { registerCoreExperiments(); onElementReadyOrDomReadyTops( '.wp-header-end', renderQuotaMeter ); renderExperimentIconsASAP(); updateRunningExperimentStatuses(); domReady( () => renderPendingPageViews() ); domReady( () => formatPageViews() ); domReady( () => renderHeaderItems( settings ) ); domReady( () => updateRunningExperimentStatuses() ); domReady( () => registerCommands() ); createStagingNotice( settings.staging ); let previousTypes: ReadonlyArray< ExperimentTypeName > = []; subscribe( debounce( () => { const types = keys( select( NAB_EXPERIMENTS ).getExperimentTypes() ); if ( areEqual( types, previousTypes ) ) { return; } previousTypes = types; renderExperimentIcons(); }, 0 ), NAB_EXPERIMENTS ); let prevQuota: Maybe< Quota >; subscribe( () => { const quota = select( NAB_DATA ).getQuota(); if ( quota === prevQuota ) { return; } prevQuota = quota; updateRunningExperimentStatuses(); }, NAB_DATA ); renderHelp(); } // ======= // HELPERS // ======= function renderPendingPageViews() { const nodes = Array.from( document.querySelectorAll< HTMLElement >( '.nab-pending-page-views-wrapper' ) ); nodes.forEach( ( node ) => { const id = Number.parseInt( node.getAttribute( 'data-id' ) || '' ); if ( isNaN( id ) ) { return; } try { void apiFetch< CloudResults >( { path: `/nab/v1/experiment/${ id }/result`, } ).then( ( results ) => { const views = toPairs( results ).reduce( ( acc, pair ) => acc + ( isAlternativeResults( pair ) ? pair[ 1 ].v || 0 : 0 ), 0 ); node.textContent = formatNumber( views ); } ); } catch ( e ) { logError( e ); node.textContent = _x( 'Unknown', 'text', 'nelio-ab-testing' ); } } ); } function formatPageViews() { const nodes = Array.from( document.querySelectorAll< HTMLElement >( '.nab-page-views-wrapper' ) ); nodes.forEach( ( node ) => { const value = Number.parseInt( node.getAttribute( 'data-value' ) || '' ); node.textContent = isNaN( value ) ? '0' : formatNumber( value ); } ); } function renderExperimentIconsASAP() { renderExperimentIcons(); setTimeout( () => { if ( 'complete' !== document.readyState ) { renderExperimentIconsASAP(); } }, 100 ); } function renderExperimentIcons() { const iconWrappers = Array.from( document.querySelectorAll< HTMLElement >( '.js-nab-experiment__icon' ) ).filter( ( wrapper ) => ! wrapper.children.length ); iconWrappers.map( ( wrapper ) => { const typeName = wrapper.getAttribute( 'data-experiment-type' ) || ''; const type = select( NAB_EXPERIMENTS ).getExperimentType( typeName ); const Icon = ( type && type.icon ) || DefaultIcon; const name = ( type && type.title ) || _x( 'Unknown', 'text (experiment type)', 'nelio-ab-testing' ); wrapper.setAttribute( 'title', name ); return render( , wrapper ); } ); } function updateRunningExperimentStatuses() { const quota = select( NAB_DATA ).getQuota(); if ( ! quota ) { return; } const running = Array.from( document.querySelectorAll< HTMLElement >( 'span.nab-experiment__status--running' ) ); const classname = 'nab-experiment__status--running-with-no-quota'; running.forEach( ( status ) => { status.classList.toggle( classname, ! quota.availableQuota ); const text = document.createElement( 'span' ); text.textContent = _x( 'Running', 'text (experiment status)', 'nelio-ab-testing' ); status.innerHTML = ''; if ( ! quota.availableQuota ) { const icon = document.createElement( 'span' ); icon.classList.add( 'dashicons', 'dashicons-warning' ); icon.setAttribute( 'title', _x( 'Event tracking has stopped because there’s no more quota available at the moment', 'text', 'nelio-ab-testing' ) ); status.appendChild( icon ); } status.appendChild( text ); } ); } function renderHeaderItems( settings: Settings ) { const pageTitle = document.querySelector( '.wp-heading-inline' ); if ( ! pageTitle?.parentElement ) { return; } const notices = document.createElement( 'div' ); const testButtonWrapper = document.createElement( 'span' ); const titleActionWrapper = document.createElement( 'span' ); pageTitle.parentElement.insertBefore( notices, pageTitle.nextSibling ); pageTitle.parentElement.insertBefore( titleActionWrapper, pageTitle.nextSibling ); pageTitle.parentElement.insertBefore( testButtonWrapper, pageTitle.nextSibling ); render( , notices ); render( , testButtonWrapper ); render( <> , titleActionWrapper ); } function renderQuotaMeter() { const headerEnd = document.querySelector< HTMLElement >( '.wp-header-end' ); if ( ! headerEnd?.parentElement ) { return; } const quotaWrapper = document.createElement( 'div' ); render( , quotaWrapper ); headerEnd.parentElement.insertBefore( quotaWrapper, headerEnd ); } const isAlternativeResults = ( pair: [ string, unknown ] ): pair is [ string, CloudAlternativeResults ] => 'a' === pair[ 0 ][ 0 ];