From 63940a9b837e433433cf0952cdc8cc50a69e95c4 Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 22:16:23 +0000 Subject: [PATCH 01/13] Network table: a Tracking switch per site (network-configured mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the network configures each site's Profile, each row now has a Tracking switch, and the network decides: the site's own Enable OWA is managed (hidden on its page, kept as stored) and its note says whether the network has tracking on or off. A site new to the network starts on; a site switched off keeps its Profile. In site-choice mode there are no switches and sites keep their own toggle. Stored as disabledSiteIds in the network option; network::save() takes enabledSites (blog ID => on) for the network's own sites only, keeping the stored state for any left out. A row counts as changed when its switch or its Profile differs from what is saved, and a saved row switched off reads "Tracking off". The select's empty choice is "— no Profile —". --- README.txt | 1 + js/lib/mapping.js | 25 ++++++++++++--- js/network/index.js | 26 ++++++++++++---- owa-plugin.php | 6 +++- src/network.php | 59 +++++++++++++++++++++++++++++++++-- src/rest.php | 1 + tests/NetworkTest.php | 62 +++++++++++++++++++++++++++++++++++++ tests/e2e/multisite.spec.js | 27 +++++++++++----- tests/js/mapping.test.js | 19 ++++++++++-- 9 files changed, 202 insertions(+), 24 deletions(-) diff --git a/README.txt b/README.txt index cf1579f..de540dd 100644 --- a/README.txt +++ b/README.txt @@ -44,6 +44,7 @@ OWA 2.x is supported in beta: set OWA Server Version to 2.x. The plugin then aut * Multisite network mode (2.x): Network Admin > OWA takes the OWA endpoint and one application password for every site. * Each site then chooses its Website ID from the Profiles that share its domain. * Or the network configures each site's Profile in a mapping table, where each site starts on the Profile of exactly its domain. Opening the network settings page fetches the Profiles from OWA as it reads the network's sites, and marks new sites and new Profiles until the next save. + * In that mode each site also has a Tracking switch in the table; the network decides, and the site's own Enable OWA is hidden. * New setting in both versions: Don't Track Logged-in Users. * The settings pages are rebuilt with WordPress components (React). They save through the plugin's REST routes; keys and passwords are never sent back to the browser, and an invalid entry is reported under its field without saving anything. diff --git a/js/lib/mapping.js b/js/lib/mapping.js index a01c8b3..7623f53 100644 --- a/js/lib/mapping.js +++ b/js/lib/mapping.js @@ -17,13 +17,19 @@ export function profilesFor( profiles, baseDomain ) { } ); } +export const OFF = 'Tracking off'; + /** - * The status of a site's row for the value now selected: the server's status - * while the row is as it was loaded, "changed" once it is not. + * The status of a site's row for its Profile and switch as they are now: + * "changed" once either differs from what was loaded; otherwise "tracking + * off" for a site switched off, or the server's status. */ -export function rowStatus( blog, value ) { +export function rowStatus( blog, value, enabled = blog.enabled ) { const loaded = blog.saved === null ? blog.selected : blog.saved; - return value === loaded ? blog.status : CHANGED; + if ( value !== loaded || enabled !== blog.enabled ) { + return CHANGED; + } + return blog.enabled === false ? OFF : blog.status; } /** The selects' values, as the network route takes them. */ @@ -35,6 +41,17 @@ export function mappingPayload( values ) { return out; } +/** The switches as they start: each site's saved on/off. */ +export function initialSwitches( blogs ) { + const out = {}; + for ( const blog of blogs ) { + if ( 'enabled' in blog ) { + out[ blog.id ] = blog.enabled; + } + } + return out; +} + /** The values the selects start on. */ export function initialMapping( blogs ) { const out = {}; diff --git a/js/network/index.js b/js/network/index.js index 47a3d97..d89aaed 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -5,16 +5,16 @@ * network's sites, so every load shows both as they are now; POST saves. */ import { createRoot, useEffect, useState } from '@wordpress/element'; -import { Button, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components'; +import { Button, FormToggle, Notice, SelectControl, Spinner, TextControl, ToggleControl } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; import { secretHint, secretsPayload } from '../lib/secrets'; -import { CHANGED, initialMapping, mappingPayload, profileLabel, profilesFor, rowStatus } from '../lib/mapping'; +import { CHANGED, initialMapping, initialSwitches, mappingPayload, profileLabel, profilesFor, rowStatus } from '../lib/mapping'; import '../settings/style.css'; const NETWORK = '/owa-wp/v1/network'; -function SitesTable( { payload, mapping, setMapping } ) { +function SitesTable( { payload, mapping, setMapping, switches, setSwitches } ) { const fetched = payload.sitesFetchedAt ? `Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).` : 'No Profiles have been fetched from OWA yet.'; @@ -27,6 +27,7 @@ function SitesTable( { payload, mapping, setMapping } ) { Site + { payload.assignProfiles && Tracking } Website ID { payload.assignProfiles && Status } @@ -43,9 +44,10 @@ function SitesTable( { payload, mapping, setMapping } ) { } const value = mapping[ blog.id ] ?? ''; - const status = rowStatus( blog, value ); + const enabled = switches[ blog.id ] ?? true; + const status = rowStatus( blog, value, enabled ); const options = [ - { label: '— not tracked —', value: '' }, + { label: '— no Profile —', value: '' }, ...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ), ]; @@ -56,6 +58,13 @@ function SitesTable( { payload, mapping, setMapping } ) { return ( { blog.host } + + setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) } + /> + - { payload.active && } + { payload.active && ( + + ) }

+ ) } + + ) : ( +

+ { savedLabel( payload.profiles, blog.saved ) } + +
+ ) } - { status } { payload.active && ( - + ) }

) } - setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) } - /> + { saved && ( + setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) } + /> + ) } ); @@ -159,7 +164,7 @@ function NetworkPage() { if ( payload.assignProfiles && assign ) { data.assignedSiteIds = mappingPayload( mapping ); - data.enabledSites = { ...switches }; + data.enabledSites = switchesPayload( payload.blogs, switches ); } setBusy( true ); diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js index 55949a6..9e77d84 100644 --- a/tests/e2e/multisite.spec.js +++ b/tests/e2e/multisite.spec.js @@ -28,13 +28,17 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await page.getByLabel( /Network configures each site's Profile/ ).check(); await save.click(); - // each site is saved on its exact match (b.wp.test has none) and shown as text with Edit + // each site is saved on its exact match and shown as text with Edit and a switch; + // b.wp.test has none, so it keeps its dropdown and has no switch await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); await expect( row( page, 'wp.test' ) ).toContainText( main ); await expect( row( page, 'a.wp.test' ) ).toContainText( `Site A – https://a.wp.test (${ siteA })` ); - await expect( row( page, 'b.wp.test' ) ).toContainText( '— no Profile —' ); await expect( site( page, 'a.wp.test' ) ).toHaveCount( 0 ); await expect( edit( page, 'a.wp.test' ) ).toBeVisible(); + await expect( tracking( page, 'a.wp.test' ) ).toBeVisible(); + await expect( site( page, 'b.wp.test' ) ).toHaveValue( '' ); + await expect( edit( page, 'b.wp.test' ) ).toHaveCount( 0 ); + await expect( tracking( page, 'b.wp.test' ) ).toHaveCount( 0 ); await shot( page, testInfo, '02-mapping-table' ); // a new Profile in OWA and a new site in the network, while the page is open @@ -48,13 +52,13 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await expect( notice( page, `New in OWA since the last visit: ${ siteB }.` ) ).toBeVisible(); await expect( notice( page, 'New sites in the network, marked below: c.wp.test.' ) ).toBeVisible(); - // the new site is not saved: marked New and shown with its dropdown + // the new site is not saved: marked New and shown with its dropdown, without a switch await expect( row( page, 'c.wp.test' ).locator( '.owa-wp-badge' ) ).toHaveText( 'New' ); await expect( site( page, 'c.wp.test' ) ).toBeVisible(); + await expect( tracking( page, 'c.wp.test' ) ).toHaveCount( 0 ); await shot( page, testInfo, '03-reloaded' ); - // Edit turns b.wp.test's Profile into a dropdown, where the new Profile is marked - await edit( page, 'b.wp.test' ).click(); + // b.wp.test's dropdown lists the new Profile, marked await expect( site( page, 'b.wp.test' ).locator( `option[value="${ siteB }"]` ) ).toHaveText( / — new$/ ); await site( page, 'b.wp.test' ).selectOption( siteB ); await shot( page, testInfo, '04-edited' ); @@ -66,9 +70,14 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await save.click(); await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); + // b.wp.test now has a saved Profile: text, Edit and its switch, on await expect( row( page, 'b.wp.test' ) ).toContainText( `Site B – https://b.wp.test (${ siteB })` ); await expect( site( page, 'b.wp.test' ) ).toHaveCount( 0 ); + await expect( tracking( page, 'b.wp.test' ) ).toBeChecked(); + // c.wp.test was saved without a Profile: no longer New, still a dropdown and no switch await expect( row( page, 'c.wp.test' ).locator( '.owa-wp-badge' ) ).toHaveCount( 0 ); + await expect( site( page, 'c.wp.test' ) ).toBeVisible(); + await expect( tracking( page, 'c.wp.test' ) ).toHaveCount( 0 ); await expect( page.getByText( ' — new' ) ).toHaveCount( 0 ); await shot( page, testInfo, '05-saved' ); diff --git a/tests/js/mapping.test.js b/tests/js/mapping.test.js index 39708d7..2fc5875 100644 --- a/tests/js/mapping.test.js +++ b/tests/js/mapping.test.js @@ -1,5 +1,5 @@ import { test, expect } from 'vitest'; -import { initialMapping, initialSwitches, isEditing, mappingPayload, profileLabel, profilesFor, savedLabel } from '../../js/lib/mapping'; +import { hasSavedProfile, initialMapping, initialSwitches, isEditing, mappingPayload, switchesPayload, profileLabel, profilesFor, savedLabel } from '../../js/lib/mapping'; const PROFILES = [ { id: 'OWA-io', label: 'PhotoPress – https://photopress.io (OWA-io)', domain: 'https://photopress.io', baseDomain: 'photopress.io', new: false }, @@ -23,11 +23,17 @@ test( 'a saved row shows its Profile as text', () => { expect( savedLabel( PROFILES, 'OWA-gone' ) ).toBe( 'OWA-gone (not in the list from OWA)' ); } ); -test( 'a row shows its dropdown when not saved yet, or after Edit', () => { +test( 'a row shows its dropdown until a Profile is saved for it, or after Edit', () => { expect( isEditing( { id: 3, saved: null }, {} ) ).toBe( true ); + expect( isEditing( { id: 3, saved: '' }, {} ) ).toBe( true ); expect( isEditing( { id: 1, saved: 'OWA-io' }, {} ) ).toBe( false ); expect( isEditing( { id: 1, saved: 'OWA-io' }, { 1: true } ) ).toBe( true ); - expect( isEditing( { id: 1, saved: '' }, { 1: false } ) ).toBe( false ); +} ); + +test( 'only a row with a saved Profile has a switch, and only those are sent', () => { + const blogs = [ { id: 1, saved: 'OWA-io' }, { id: 3, saved: '' }, { id: 5, saved: null } ]; + expect( blogs.map( hasSavedProfile ) ).toEqual( [ true, false, false ] ); + expect( switchesPayload( blogs, { 1: false, 3: true, 5: true } ) ).toEqual( { 1: false } ); } ); test( 'the selects start where the server put them and are sent by site', () => { From d647b520ceb44c495ad2c71e403924d1f6aea0ba Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 23:33:55 +0000 Subject: [PATCH 08/13] =?UTF-8?q?Network=20table:=20the=20empty=20choice?= =?UTF-8?q?=20reads=20"Select=20Observation=20Profile=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/network/index.js | 2 +- tests/e2e/multisite.spec.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/js/network/index.js b/js/network/index.js index d9adfec..87eb525 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -53,7 +53,7 @@ function SitesTable( { payload, mapping, setMapping, switches, setSwitches, edit const setEdit = ( on ) => setEditing( ( current ) => ( { ...current, [ blog.id ]: on } ) ); const options = [ - { label: '— no Profile —', value: '' }, + { label: 'Select Observation Profile…', value: '' }, ...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ), ]; diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js index 9e77d84..738e9cc 100644 --- a/tests/e2e/multisite.spec.js +++ b/tests/e2e/multisite.spec.js @@ -37,6 +37,7 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await expect( edit( page, 'a.wp.test' ) ).toBeVisible(); await expect( tracking( page, 'a.wp.test' ) ).toBeVisible(); await expect( site( page, 'b.wp.test' ) ).toHaveValue( '' ); + await expect( site( page, 'b.wp.test' ).locator( 'option:checked' ) ).toHaveText( 'Select Observation Profile…' ); await expect( edit( page, 'b.wp.test' ) ).toHaveCount( 0 ); await expect( tracking( page, 'b.wp.test' ) ).toHaveCount( 0 ); await shot( page, testInfo, '02-mapping-table' ); From 3a21d3af48e2500e78b5434daa9edd1a08d49d94 Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 23:35:06 +0000 Subject: [PATCH 09/13] Network table: the column is "Observation Profile", not "Website ID" --- js/network/index.js | 6 +++--- tests/e2e/multisite.spec.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/network/index.js b/js/network/index.js index 87eb525..9922250 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -27,7 +27,7 @@ function SitesTable( { payload, mapping, setMapping, switches, setSwitches, edit Site - Website ID + Observation Profile { payload.assignProfiles && Tracking } @@ -73,7 +73,7 @@ function SitesTable( { payload, mapping, setMapping, switches, setSwitches, edit { savedLabel( payload.profiles, blog.saved ) } - diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js index 738e9cc..1e1c09d 100644 --- a/tests/e2e/multisite.spec.js +++ b/tests/e2e/multisite.spec.js @@ -8,8 +8,8 @@ const { env, login, notice, shot, owaSiteId, owaCli, wp } = require( './helpers' const NETWORK_PAGE = 'http://wp.test/wp-admin/network/admin.php?page=owa-wordpress-network'; const row = ( page, host ) => page.locator( `tr[data-host="${ host }"]` ); -const site = ( page, host ) => page.getByLabel( `Website ID for ${ host }`, { exact: true } ); -const edit = ( page, host ) => page.getByRole( 'button', { name: `Edit the Website ID for ${ host }`, exact: true } ); +const site = ( page, host ) => page.getByLabel( `Observation Profile for ${ host }`, { exact: true } ); +const edit = ( page, host ) => page.getByRole( 'button', { name: `Edit the Observation Profile for ${ host }`, exact: true } ); const tracking = ( page, host ) => page.getByLabel( `Track ${ host }`, { exact: true } ); test( 'the network configures each site\'s Profile', async ( { page, browser }, testInfo ) => { From 6ef3fd4e2ac42fae3ecd771783c855d5a819864d Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 23:39:32 +0000 Subject: [PATCH 10/13] =?UTF-8?q?Network=20table:=20the=20empty=20choice?= =?UTF-8?q?=20reads=20"Select=20Profile=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/network/index.js | 2 +- tests/e2e/multisite.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/network/index.js b/js/network/index.js index 9922250..307958e 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -53,7 +53,7 @@ function SitesTable( { payload, mapping, setMapping, switches, setSwitches, edit const setEdit = ( on ) => setEditing( ( current ) => ( { ...current, [ blog.id ]: on } ) ); const options = [ - { label: 'Select Observation Profile…', value: '' }, + { label: 'Select Profile…', value: '' }, ...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ), ]; diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js index 1e1c09d..52377d2 100644 --- a/tests/e2e/multisite.spec.js +++ b/tests/e2e/multisite.spec.js @@ -37,7 +37,7 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await expect( edit( page, 'a.wp.test' ) ).toBeVisible(); await expect( tracking( page, 'a.wp.test' ) ).toBeVisible(); await expect( site( page, 'b.wp.test' ) ).toHaveValue( '' ); - await expect( site( page, 'b.wp.test' ).locator( 'option:checked' ) ).toHaveText( 'Select Observation Profile…' ); + await expect( site( page, 'b.wp.test' ).locator( 'option:checked' ) ).toHaveText( 'Select Profile…' ); await expect( edit( page, 'b.wp.test' ) ).toHaveCount( 0 ); await expect( tracking( page, 'b.wp.test' ) ).toHaveCount( 0 ); await shot( page, testInfo, '02-mapping-table' ); From b374ac8e4f831b7f9b8740bd490d6bbdaafa9068 Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 23:42:51 +0000 Subject: [PATCH 11/13] "Observation Profile" wherever 2.x is meant, as OWA 2.x calls it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The network page, the site page in 2.x mode, their notes and notices say "Observation Profile" (placeholder "Select Profile…"). 1.x keeps "Website ID", which the one remaining "Site ID" notice now says too. A field can carry title_v2 / description_v2; the site page uses them while 2.x is chosen, so the Website ID field relabels as the version switch flips. --- js/lib/fields.js | 6 ++++++ js/lib/mapping.js | 2 +- js/network/index.js | 10 +++++----- js/settings/index.js | 15 +++++++++------ owa-plugin.php | 18 +++++++++++------- src/network.php | 10 +++++----- src/rest.php | 7 ++++++- tests/NetworkTest.php | 14 +++++++------- tests/OwaV2Test.php | 2 +- tests/RestTest.php | 2 +- tests/e2e/multisite.spec.js | 18 +++++++++--------- tests/e2e/single.spec.js | 20 ++++++++++++-------- tests/js/fields.test.js | 9 ++++++++- tests/js/mapping.test.js | 2 +- 14 files changed, 82 insertions(+), 53 deletions(-) diff --git a/js/lib/fields.js b/js/lib/fields.js index 2f2e959..a7f3274 100644 --- a/js/lib/fields.js +++ b/js/lib/fields.js @@ -16,6 +16,12 @@ export function visibleFields( fields, version, section ) { return fields.filter( ( field ) => isVisible( field, version ) && ( ! section || field.section === section ) ); } +/** A field's title and description for the chosen version: 2.x has its own wording for some. */ +export function fieldText( field, version ) { + const v2 = version === '2' ? field.v2 || {} : {}; + return { title: v2.title || field.title, description: v2.description || field.description }; +} + /** The tabs that have something to show. */ export function visibleSections( sections, fields, version ) { return sections.filter( ( section ) => visibleFields( fields, version, section.name ).length > 0 ); diff --git a/js/lib/mapping.js b/js/lib/mapping.js index dc8e60c..1ca000e 100644 --- a/js/lib/mapping.js +++ b/js/lib/mapping.js @@ -18,7 +18,7 @@ export function profilesFor( profiles, baseDomain ) { /** A saved row's Profile, as its cell shows it until Edit is clicked. */ export function savedLabel( profiles, id ) { if ( ! id ) { - return '— no Profile —'; + return 'No Observation Profile'; } const profile = profiles.find( ( p ) => p.id === id ); return profile ? profile.label : `${ id } (not in the list from OWA)`; diff --git a/js/network/index.js b/js/network/index.js index 307958e..e5865b5 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -16,8 +16,8 @@ const NETWORK = '/owa-wp/v1/network'; function SitesTable( { payload, mapping, setMapping, switches, setSwitches, editing, setEditing } ) { const fetched = payload.sitesFetchedAt - ? `Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).` - : 'No Profiles have been fetched from OWA yet.'; + ? `Observation Profiles last fetched from OWA: ${ new Date( payload.sitesFetchedAt * 1000 ).toISOString().slice( 0, 16 ).replace( 'T', ' ' ) } UTC (${ payload.profiles.length }).` + : 'No Observation Profiles have been fetched from OWA yet.'; return ( <> @@ -212,7 +212,7 @@ function NetworkPage() { __next40pxDefaultSize __nextHasNoMarginBottom label="Application Password" - help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties." + help="An application password from OWA (My Preferences > Application Passwords). Site admins can list the Observation Profiles its OWA user can see, so use a read-only password of a user limited to this network's Properties." type="password" autoComplete="off" value={ password.value || '' } @@ -232,11 +232,11 @@ function NetworkPage() {

{ sites.notice &&

{ sites.notice }

} { payload.network.note &&

{ payload.network.note }

} { showTagSettingsLink( payload, values ) && (

- Configure this Website ID's tracking tag in OWA + Configure this Observation Profile's tracking tag in OWA

) } @@ -58,7 +60,8 @@ function WebsiteId( { field, value, onChange, sites, payload, values } ) { function Field( { field, payload, values, setValue, secrets, setSecret, sites, error } ) { const value = values[ field.key ]; - const common = { label: field.title, help: field.description }; + const text = fieldText( field, values.owaVersion ); + const common = { label: text.title, help: text.description }; switch ( field.type ) { case 'radio': diff --git a/owa-plugin.php b/owa-plugin.php index 57ea626..b167fa8 100644 --- a/owa-plugin.php +++ b/owa-plugin.php @@ -166,7 +166,9 @@ function _init() { // remove this if uneeded if ( ! $this->isOwaReadyToTrack() ) { - $this->adminMsgs[] = ['message' => 'Open Web Analytics requires that you select a Site ID before tracking can begin.', 'class' => 'notice-warning']; + $this->adminMsgs[] = ['message' => $this->getOwaVersion() === 2 + ? 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' + : 'Open Web Analytics requires that you select a Website ID before tracking can begin.', 'class' => 'notice-warning']; } @@ -508,7 +510,7 @@ function initOwaSdk() { if ( ! $this->isOwaSdkReady() ) { $this->addNag( $this->getOwaVersion() === 2 - ? 'Enter your OWA Endpoint and an Application Password to load the list of websites from OWA.' + ? 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.' : 'You must enter your OWA API key, Secret Auth key, and Endpoint settings.', 'notice-warning' ); return; @@ -688,7 +690,7 @@ function filterSitesForNetwork( array $sites ) { if ( $assigned === '' ) { - $this->sitesNotice = 'The network administrator has not assigned this site an OWA Profile, so it is not tracked.'; + $this->sitesNotice = 'The network administrator has not assigned this site an Observation Profile, so it is not tracked.'; } return array_filter( $sites, fn( $site ) => owaWp\network::rowSiteId( $site ) === $assigned ); @@ -706,8 +708,8 @@ function getNetworkNote() { } return owaWp\network::options()['assignProfiles'] - ? sprintf( 'The network administrator manages the OWA server (%s), configures this site\'s Profile and has tracking on this site turned %s.', $this->getOption( 'owaEndpoint' ), $this->getOption( 'enable' ) ? 'on' : 'off' ) - : sprintf( 'The network administrator manages the OWA server (%s) and its credentials. The list shows the Profiles that share this site\'s domain.', $this->getOption( 'owaEndpoint' ) ); + ? sprintf( 'The network administrator manages the OWA server (%s), configures this site\'s Observation Profile and has tracking on this site turned %s.', $this->getOption( 'owaEndpoint' ), $this->getOption( 'enable' ) ? 'on' : 'off' ) + : sprintf( 'The network administrator manages the OWA server (%s) and its credentials. The list shows the Observation Profiles that share this site\'s domain.', $this->getOption( 'owaEndpoint' ) ); } /** @@ -776,7 +778,7 @@ public function registerOptions() { 'title' => 'OWA Server Version', 'page_name' => 'owa-wordpress', 'section' => 'general', - 'description' => 'Which Open Web Analytics server this site reports to. 2.x (beta) loads the Website ID's tracking bundle; its tracker settings are made on the OWA server.', + 'description' => 'Which Open Web Analytics server this site reports to. 2.x (beta) loads the Observation Profile's tracking bundle; its tracker settings are made on the OWA server.', 'label_for' => 'OWA server version', 'options' => [ '1' => '1.x', '2' => '2.x (beta)' ], 'error_message' => '' @@ -871,9 +873,11 @@ public function registerOptions() { 'field' => array( 'type' => 'select', 'title' => 'Website ID', + 'title_v2' => 'Observation Profile', 'page_name' => 'owa-wordpress', 'section' => 'general', 'description' => 'Select the ID of the website you want to track. New tracked websites can be added via the OWA server admin interface.', + 'description_v2' => 'Select the Observation Profile you want to track. New Observation Profiles can be added in OWA.', 'label_for' => 'Tracked website ID', 'length' => 90, 'error_message' => '', @@ -1053,7 +1057,7 @@ function getSitesFromOwa( $options = [] ) { if ( ! is_array( $sites ) ) { $errors = $sites_client->getLastErrors(); - $this->sitesNotice = 'Could not load the list of websites from OWA: ' . ( $errors ? implode( '; ', $errors ) : 'no response' ); + $this->sitesNotice = sprintf( 'Could not load the list of %s from OWA: ', $this->getOwaVersion() === 2 ? 'Observation Profiles' : 'websites' ) . ( $errors ? implode( '; ', $errors ) : 'no response' ); } } diff --git a/src/network.php b/src/network.php index 9be9cb0..d0a873c 100644 --- a/src/network.php +++ b/src/network.php @@ -323,7 +323,7 @@ public static function sanitizeMapping( array $submitted, array $sites, array $m } else { - $errors[] = sprintf( '%s: %s is not in the list fetched from OWA, so its Profile was not changed.', $blog_hosts[ $blog_id ], $site_id ); + $errors[] = sprintf( '%s: %s is not in the list fetched from OWA, so its Observation Profile was not changed.', $blog_hosts[ $blog_id ], $site_id ); } } @@ -480,8 +480,8 @@ public static function fetchProfiles( $plugin, array &$options ) { return [ 'info' => [], - 'error' => 'Could not load the list of websites from OWA: ' . $plugin->getLastSitesError() - . ( $fetched ? sprintf( '. Showing the Profiles fetched %s UTC.', gmdate( 'Y-m-d H:i', $fetched ) ) : '.' ), + 'error' => 'Could not load the list of Observation Profiles from OWA: ' . $plugin->getLastSitesError() + . ( $fetched ? sprintf( '. Showing the Observation Profiles fetched %s UTC.', gmdate( 'Y-m-d H:i', $fetched ) ) : '.' ), ]; } @@ -497,12 +497,12 @@ public static function fetchProfiles( $plugin, array &$options ) { if ( $before && $change['new'] ) { - $info[] = 'New in OWA since the last visit: ' . implode( ', ', $change['new'] ) . '.'; + $info[] = 'New Observation Profiles in OWA since the last visit: ' . implode( ', ', $change['new'] ) . '.'; } if ( $change['gone'] ) { - $info[] = 'No longer in OWA: ' . implode( ', ', $change['gone'] ) . '.'; + $info[] = 'Observation Profiles no longer in OWA: ' . implode( ', ', $change['gone'] ) . '.'; } return [ 'info' => $info, 'error' => '' ]; diff --git a/src/rest.php b/src/rest.php index 55de49c..04b4cd1 100644 --- a/src/rest.php +++ b/src/rest.php @@ -68,6 +68,11 @@ public static function settingsPayload( $plugin ) { 'type' => $type, 'title' => html_entity_decode( (string) $field['title'], ENT_QUOTES, 'UTF-8' ), 'description' => html_entity_decode( (string) ( $field['description'] ?? '' ), ENT_QUOTES, 'UTF-8' ), + // wording for 2.x, where it differs (the Website ID is an Observation Profile there) + 'v2' => array_map( + fn( $text ) => html_entity_decode( (string) $text, ENT_QUOTES, 'UTF-8' ), + array_filter( [ 'title' => $field['title_v2'] ?? '', 'description' => $field['description_v2'] ?? '' ] ) + ), 'section' => $field['section'] ?? 'general', 'version' => $field['owa_version'] ?? null, 'options' => self::choices( $field['options'] ?? [] ), @@ -244,7 +249,7 @@ public static function sanitizeSettings( array $schema, array $input, array $sto && (string) ( $out['siteId'] ?? '' ) !== '' && ! preg_match( \owaWp_plugin::BUNDLE_SITE_ID_PATTERN, (string) $out['siteId'] ) ) { - $errors['siteId'] = 'OWA 2.x cannot publish a tracking bundle for this Website ID.'; + $errors['siteId'] = 'OWA 2.x cannot publish a tracking bundle for this Observation Profile.'; } return [ $out, $errors ]; diff --git a/tests/NetworkTest.php b/tests/NetworkTest.php index d81b2ae..5632c9e 100644 --- a/tests/NetworkTest.php +++ b/tests/NetworkTest.php @@ -150,7 +150,7 @@ public function testASubmittedProfileMustBeInTheFetchedList(): void { ); $this->assertSame( [ 3 => 'OWA-old', 1 => 'OWA-0' ], $map, 'Blog 9 is not in the network.' ); - $this->assertSame( [ 'archive.peteradamsphoto.com: OWA-gone is not in the list fetched from OWA, so its Profile was not changed.' ], $errors ); + $this->assertSame( [ 'archive.peteradamsphoto.com: OWA-gone is not in the list fetched from OWA, so its Observation Profile was not changed.' ], $errors ); } public function testNetworkModeNeedsAMultisiteAndBothSettings(): void { @@ -253,7 +253,7 @@ public function testAConfiguredSitesPageShowsItsWebsiteIdReadOnly(): void { $this->assertTrue( $fields['owaEndpoint']['managed'] ); $this->assertFalse( $fields['owaEndpoint']['readonly'], 'Managed and not read-only: hidden.' ); $this->assertSame( 'OWA-archive', $payload['values']['siteId'] ); - $this->assertStringContainsString( 'configures this site\'s Profile', $payload['network']['note'] ); + $this->assertStringContainsString( 'configures this site\'s Observation Profile', $payload['network']['note'] ); } private function pluginListing( array $rows ): \owaWp_plugin { @@ -305,7 +305,7 @@ public function testAnUnassignedSiteSaysSo(): void { $plugin = $this->pluginListing( self::rows( 'https://photopress.io' ) ); $this->assertSame( [], $plugin->getSitesFromOwa() ); - $this->assertSame( 'The network administrator has not assigned this site an OWA Profile, so it is not tracked.', $plugin->getSitesNotice() ); + $this->assertSame( 'The network administrator has not assigned this site an Observation Profile, so it is not tracked.', $plugin->getSitesNotice() ); } // ---- the network settings ------------------------------------------------ @@ -403,7 +403,7 @@ public function testAProfileNotInTheListIsRefusedAndTheRestSaved(): void { [ , $errors ] = network::save( $this->credentials( [ 'assignedSiteIds' => [ 1 => 'OWA-gone', 3 => 'OWA-www' ] ] ), $this->networkPlugin() ); $this->assertSame( [ 1 => 'OWA-main', 3 => 'OWA-www' ], $this->network['assignedSiteIds'] ); - $this->assertSame( [ 'photopress.io: OWA-gone is not in the list fetched from OWA, so its Profile was not changed.' ], $errors['assignedSiteIds'] ); + $this->assertSame( [ 'photopress.io: OWA-gone is not in the list fetched from OWA, so its Observation Profile was not changed.' ], $errors['assignedSiteIds'] ); } public function testOpeningThePageFetchesTheProfilesAndKeepsTheMapping(): void { @@ -433,8 +433,8 @@ public function testOpeningThePageSaysWhatIsNewAndWhatIsGone(): void { $this->assertSame( [ 'OWA-arch', 'OWA-www' ], $this->network['newProfileIds'] ); $this->assertSame( [ - 'New in OWA since the last visit: OWA-arch, OWA-www.', - 'No longer in OWA: OWA-old.', + 'New Observation Profiles in OWA since the last visit: OWA-arch, OWA-www.', + 'Observation Profiles no longer in OWA: OWA-old.', 'New sites in the network, marked below: archive.peteradamsphoto.com.', ], $result['info'] ); } @@ -468,7 +468,7 @@ public function getLastSitesError(): string { return 'Could not resolve host'; } $result = network::load( $down ); - $this->assertSame( 'Could not load the list of websites from OWA: Could not resolve host. Showing the Profiles fetched 2026-10-14 17:46 UTC.', $result['error'] ); + $this->assertSame( 'Could not load the list of Observation Profiles from OWA: Could not resolve host. Showing the Observation Profiles fetched 2026-10-14 17:46 UTC.', $result['error'] ); $this->assertSame( $before, $this->network, 'The cached Profiles stay.' ); } diff --git a/tests/OwaV2Test.php b/tests/OwaV2Test.php index e998131..bd2bd2e 100644 --- a/tests/OwaV2Test.php +++ b/tests/OwaV2Test.php @@ -142,7 +142,7 @@ public function testAnHttpEndpointIsReportedUnderThePicker(): void { } $this->assertSame( [ 'OWA-9b62dc7ce78a6934' ], array_keys( $sites ), 'The saved ID stays selectable.' ); - $this->assertSame( 'Could not load the list of websites from OWA: Application password not sent: instance_url is not https', $plugin->getSitesNotice() ); + $this->assertSame( 'Could not load the list of Observation Profiles from OWA: Application password not sent: instance_url is not https', $plugin->getSitesNotice() ); } public function testTheSdkConfigCanBeFiltered(): void { diff --git a/tests/RestTest.php b/tests/RestTest.php index fca17bc..ef5cc25 100644 --- a/tests/RestTest.php +++ b/tests/RestTest.php @@ -76,7 +76,7 @@ public function testA2xWebsiteIdMustBeABundleFileName(): void { [ , $errors ] = rest::sanitizeSettings( $this->schema(), [ 'owaVersion' => '2', 'siteId' => '../owa-config' ], [] ); - $this->assertSame( 'OWA 2.x cannot publish a tracking bundle for this Website ID.', $errors['siteId'] ); + $this->assertSame( 'OWA 2.x cannot publish a tracking bundle for this Observation Profile.', $errors['siteId'] ); } public function testSecretsAreKeptUnlessReplacedOrCleared(): void { diff --git a/tests/e2e/multisite.spec.js b/tests/e2e/multisite.spec.js index 52377d2..13fb9a5 100644 --- a/tests/e2e/multisite.spec.js +++ b/tests/e2e/multisite.spec.js @@ -25,7 +25,7 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, await page.getByLabel( 'OWA Endpoint' ).fill( env( 'OWA_URL' ) ); await page.getByLabel( 'Application Password' ).fill( env( 'OWA_APP_PASSWORD' ) ); - await page.getByLabel( /Network configures each site's Profile/ ).check(); + await page.getByLabel( /Network configures each site's Observation Profile/ ).check(); await save.click(); // each site is saved on its exact match and shown as text with Edit and a switch; @@ -51,7 +51,7 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, // reloading the page fetches the Profiles and reads the sites together await page.reload(); - await expect( notice( page, `New in OWA since the last visit: ${ siteB }.` ) ).toBeVisible(); + await expect( notice( page, `New Observation Profiles in OWA since the last visit: ${ siteB }.` ) ).toBeVisible(); await expect( notice( page, 'New sites in the network, marked below: c.wp.test.' ) ).toBeVisible(); // the new site is not saved: marked New and shown with its dropdown, without a switch await expect( row( page, 'c.wp.test' ).locator( '.owa-wp-badge' ) ).toHaveText( 'New' ); @@ -96,14 +96,14 @@ test( 'the network configures each site\'s Profile', async ( { page, browser }, // the site shows what the network assigned, read-only, and none of the network's fields await page.goto( 'http://a.wp.test/wp-admin/admin.php?page=owa-wordpress' ); - await expect( page.getByLabel( 'Website ID', { exact: true } ) ).toBeDisabled(); - await expect( page.getByLabel( 'Website ID', { exact: true } ) ).toHaveValue( siteA ); + await expect( page.getByLabel( 'Observation Profile', { exact: true } ) ).toBeDisabled(); + await expect( page.getByLabel( 'Observation Profile', { exact: true } ) ).toHaveValue( siteA ); // labelled from OWA's list, though the network has tracking off here - await expect( page.getByLabel( 'Website ID', { exact: true } ).locator( 'option:checked' ) ).toHaveText( /^Site A – https:\/\/a\.wp\.test/ ); + await expect( page.getByLabel( 'Observation Profile', { exact: true } ).locator( 'option:checked' ) ).toHaveText( /^Site A – https:\/\/a\.wp\.test/ ); await expect( page.getByLabel( 'OWA Endpoint' ) ).toHaveCount( 0 ); await expect( page.getByLabel( 'Application Password' ) ).toHaveCount( 0 ); await expect( page.getByLabel( '2.x (beta)' ) ).toHaveCount( 0 ); - await expect( page.getByText( 'configures this site\'s Profile and has tracking on this site turned off' ) ).toBeVisible(); + await expect( page.getByText( 'configures this site\'s Observation Profile and has tracking on this site turned off' ) ).toBeVisible(); await expect( page.getByLabel( 'Enable OWA' ) ).toHaveCount( 0 ); await shot( page, testInfo, '06-site-assigned' ); @@ -122,7 +122,7 @@ test( 'each site picks from the Profiles sharing its domain', async ( { page }, await login( page ); await page.goto( NETWORK_PAGE ); - await page.getByLabel( /Network configures each site's Profile/ ).uncheck(); + await page.getByLabel( /Network configures each site's Observation Profile/ ).uncheck(); await expect( page.getByText( 'Save to switch.' ) ).toBeVisible(); await page.getByRole( 'button', { name: 'Save Changes' } ).click(); await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); @@ -130,7 +130,7 @@ test( 'each site picks from the Profiles sharing its domain', async ( { page }, await shot( page, testInfo, '07-network-site-choice' ); await page.goto( 'http://b.wp.test/wp-admin/admin.php?page=owa-wordpress' ); - const picker = page.getByLabel( 'Website ID', { exact: true } ); + const picker = page.getByLabel( 'Observation Profile', { exact: true } ); await expect( picker.locator( 'option' ) ).not.toHaveCount( 1 ); await expect( picker ).toBeEnabled(); @@ -138,6 +138,6 @@ test( 'each site picks from the Profiles sharing its domain', async ( { page }, const labels = await picker.locator( 'option' ).allTextContents(); expect( labels.some( ( v ) => v.includes( 'https://a.wp.test' ) ) ).toBe( true ); expect( labels.some( ( v ) => v.includes( 'other.example' ) ) ).toBe( false ); - await expect( page.getByText( 'The list shows the Profiles that share this site\'s domain.' ) ).toBeVisible(); + await expect( page.getByText( 'The list shows the Observation Profiles that share this site\'s domain.' ) ).toBeVisible(); await shot( page, testInfo, '08-site-choice' ); } ); diff --git a/tests/e2e/single.spec.js b/tests/e2e/single.spec.js index fd1d37c..90e25ce 100644 --- a/tests/e2e/single.spec.js +++ b/tests/e2e/single.spec.js @@ -17,6 +17,7 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await expect( field( '1.x' ) ).toBeChecked(); await expect( field( 'API Key' ) ).toBeVisible(); await expect( field( 'Application Password' ) ).toHaveCount( 0 ); + await expect( field( 'Website ID' ) ).toBeVisible(); await shot( page, testInfo, '01-settings-1x' ); await tab( 'Tracking' ).click(); @@ -29,6 +30,9 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await expect( field( 'API Key' ) ).toHaveCount( 0 ); await expect( field( 'Application Password' ) ).toBeVisible(); await expect( tab( 'Advanced' ) ).toHaveCount( 0 ); + // the same field is the Observation Profile in 2.x + await expect( field( 'Website ID' ) ).toHaveCount( 0 ); + await expect( field( 'Observation Profile' ).locator( 'option:checked' ) ).toHaveText( /^(Loading|Select Profile)…$/ ); await shot( page, testInfo, '03-switched-to-2x' ); await tab( 'Tracking' ).click(); @@ -52,27 +56,27 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await expect( page.getByText( 'Saved. Enter a new one to replace it.' ) ).toBeVisible(); // the list comes from OWA with the application password - await expect( field( 'Website ID' ).locator( `option[value="${ siteId }"]` ) ).toHaveCount( 1 ); - await field( 'Website ID' ).selectOption( siteId ); + await expect( field( 'Observation Profile' ).locator( `option[value="${ siteId }"]` ) ).toHaveCount( 1 ); + await field( 'Observation Profile' ).selectOption( siteId ); // chosen but not saved: no link to OWA's tracking tag screen yet - const tagLink = page.getByRole( 'link', { name: /Configure this Website ID's tracking tag in OWA/ } ); + const tagLink = page.getByRole( 'link', { name: /Configure this Observation Profile's tracking tag in OWA/ } ); await expect( tagLink ).toHaveCount( 0 ); await save.click(); await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); await expect( tagLink ).toBeVisible(); await shot( page, testInfo, '06-2x-saved' ); - // choosing another Website ID hides it again until that is saved - await field( 'Website ID' ).selectOption( '' ); + // choosing another Observation Profile hides it again until that is saved + await field( 'Observation Profile' ).selectOption( '' ); await expect( tagLink ).toHaveCount( 0 ); - await field( 'Website ID' ).selectOption( siteId ); + await field( 'Observation Profile' ).selectOption( siteId ); await expect( tagLink ).toBeVisible(); // the page loads saved, and the password is not in it await page.reload(); - await expect( field( 'Website ID' ) ).toHaveValue( siteId ); - await expect( field( 'Website ID' ).locator( 'option:checked' ) ).not.toHaveText( /not in the list from OWA/ ); + await expect( field( 'Observation Profile' ) ).toHaveValue( siteId ); + await expect( field( 'Observation Profile' ).locator( 'option:checked' ) ).not.toHaveText( /not in the list from OWA/ ); expect( await page.content() ).not.toContain( env( 'OWA_APP_PASSWORD' ) ); // a visitor gets the bundle tag, and the bundle loads diff --git a/tests/js/fields.test.js b/tests/js/fields.test.js index b4386d9..0c07a2c 100644 --- a/tests/js/fields.test.js +++ b/tests/js/fields.test.js @@ -1,5 +1,5 @@ import { test, expect } from 'vitest'; -import { isVisible, showTagSettingsLink, visibleFields, visibleSections } from '../../js/lib/fields'; +import { fieldText, isVisible, showTagSettingsLink, visibleFields, visibleSections } from '../../js/lib/fields'; const FIELDS = [ { key: 'owaVersion', section: 'general', version: null }, @@ -43,3 +43,10 @@ test( 'the link to OWA\'s tracking tag screen shows only for the saved Website I expect( showTagSettingsLink( payload, { ...saved, owaEndpoint: 'https://other.example.com/' } ) ).toBe( false ); expect( showTagSettingsLink( { ...payload, tagSettingsUrl: '' }, saved ) ).toBe( false ); } ); + +test( 'a field takes its 2.x wording only while 2.x is chosen', () => { + const siteId = { title: 'Website ID', description: 'Pick a website.', v2: { title: 'Observation Profile', description: 'Pick a Profile.' } }; + expect( fieldText( siteId, '1' ) ).toEqual( { title: 'Website ID', description: 'Pick a website.' } ); + expect( fieldText( siteId, '2' ) ).toEqual( { title: 'Observation Profile', description: 'Pick a Profile.' } ); + expect( fieldText( { title: 'OWA Endpoint', description: 'The URL.', v2: [] }, '2' ) ).toEqual( { title: 'OWA Endpoint', description: 'The URL.' } ); +} ); diff --git a/tests/js/mapping.test.js b/tests/js/mapping.test.js index 2fc5875..dba9bdb 100644 --- a/tests/js/mapping.test.js +++ b/tests/js/mapping.test.js @@ -19,7 +19,7 @@ test( 'a site lists the Profiles of its base domain first', () => { test( 'a saved row shows its Profile as text', () => { expect( savedLabel( PROFILES, 'OWA-arch' ) ).toBe( PROFILES[ 1 ].label ); - expect( savedLabel( PROFILES, '' ) ).toBe( '— no Profile —' ); + expect( savedLabel( PROFILES, '' ) ).toBe( 'No Observation Profile' ); expect( savedLabel( PROFILES, 'OWA-gone' ) ).toBe( 'OWA-gone (not in the list from OWA)' ); } ); From 3519d385434a908c02bbe5798b8b07c56483fbb5 Mon Sep 17 00:00:00 2001 From: padams Date: Wed, 21 Oct 2026 23:45:54 +0000 Subject: [PATCH 12/13] Integration: the 2.x listing error names Observation Profiles --- tests/Integration/OwaV2ServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/OwaV2ServerTest.php b/tests/Integration/OwaV2ServerTest.php index a114a95..d03ce1a 100644 --- a/tests/Integration/OwaV2ServerTest.php +++ b/tests/Integration/OwaV2ServerTest.php @@ -98,7 +98,7 @@ public function testAWrongApplicationPasswordIsReportedUnderThePicker(): void { $sites = $plugin->getSitesFromOwa(); $this->assertSame( [ 'saved1' ], array_keys( $sites ) ); - $this->assertStringStartsWith( 'Could not load the list of websites from OWA: ', $plugin->getSitesNotice() ); + $this->assertStringStartsWith( 'Could not load the list of Observation Profiles from OWA: ', $plugin->getSitesNotice() ); } public function testTheTrackingTagLinkIsARealScreen(): void { From f8b8c6ce841d3175a5033580699598ada40f363a Mon Sep 17 00:00:00 2001 From: padams Date: Thu, 22 Oct 2026 00:33:35 +0000 Subject: [PATCH 13/13] Settings page: setup notices follow the chosen version and the last save The "enter your credentials" and "select a Website ID" notices were printed by WordPress at page load, worded for the saved version, and stayed after a save until a reload. setupNotices() now words them for a version; the settings route sends both versions' notices with every load and save, and the page shows the ones for the version chosen on it. Other admin screens still get them from admin_notices. Where the network sets the Observation Profile, "select one" is not shown: the site admin cannot, and the page already says why. --- js/lib/fields.js | 11 +++++ js/settings/index.js | 7 +++- owa-plugin.php | 87 +++++++++++++++++++++++++++++++--------- src/rest.php | 2 + tests/PluginTest.php | 42 +++++++++++++++++++ tests/TestCase.php | 1 + tests/e2e/single.spec.js | 12 ++++++ tests/js/fields.test.js | 9 ++++- 8 files changed, 150 insertions(+), 21 deletions(-) diff --git a/js/lib/fields.js b/js/lib/fields.js index a7f3274..67cc667 100644 --- a/js/lib/fields.js +++ b/js/lib/fields.js @@ -22,6 +22,17 @@ export function fieldText( field, version ) { return { title: v2.title || field.title, description: v2.description || field.description }; } +/** + * What is left to set up, as the server found it in the saved settings, + * worded for the version chosen on the page; none while tracking is off. + */ +export function setupNotices( payload, values ) { + if ( ! Number( values.enable ) ) { + return []; + } + return ( payload.notices || {} )[ values.owaVersion === '2' ? '2' : '1' ] || []; +} + /** The tabs that have something to show. */ export function visibleSections( sections, fields, version ) { return sections.filter( ( section ) => visibleFields( fields, version, section.name ).length > 0 ); diff --git a/js/settings/index.js b/js/settings/index.js index 909007b..d4821cd 100644 --- a/js/settings/index.js +++ b/js/settings/index.js @@ -19,7 +19,7 @@ import { } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; -import { fieldText, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields'; +import { fieldText, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../lib/fields'; import { secretHint, secretsPayload } from '../lib/secrets'; import './style.css'; @@ -182,6 +182,11 @@ function SettingsPage() { { notice.text } ) } + { setupNotices( payload, values ).map( ( text ) => ( + + { text } + + ) ) }

Settings for integrating WordPress with an existing Open Web Analytics (OWA) server instance.

tab.name ).join() }> { ( tab ) => ( diff --git a/owa-plugin.php b/owa-plugin.php index b167fa8..8f6f562 100644 --- a/owa-plugin.php +++ b/owa-plugin.php @@ -163,15 +163,6 @@ function _init() { } - // remove this if uneeded - if ( ! $this->isOwaReadyToTrack() ) { - - $this->adminMsgs[] = ['message' => $this->getOwaVersion() === 2 - ? 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' - : 'Open Web Analytics requires that you select a Website ID before tracking can begin.', 'class' => 'notice-warning']; - - } - add_action('admin_notices', array( $this, 'showNag') ); } @@ -206,9 +197,20 @@ function showNag() { ); - if ( $this->adminMsgs ) { + $messages = $this->adminMsgs; + + // the settings page shows these itself, for the version chosen on it + if ( ! $this->isSettingsScreen() ) { + + foreach ( $this->setupNotices() as $notice ) { + + $messages[] = [ 'message' => $notice, 'class' => 'notice-warning' ]; + } + } + + if ( $messages ) { - foreach ( $this->adminMsgs as $msg ) { + foreach ( $messages as $msg ) { $message = wp_kses( $msg['message'], $allowed_html ); @@ -465,7 +467,12 @@ function getOwaVersion() { return (string) $this->getOption( 'owaVersion' ) === '2' ? 2 : 1; } - function isOwaReadyToTrack() { + /** + * @param int|null $version 1 or 2; the saved version by default + */ + function isOwaReadyToTrack( $version = null ) { + + $version = $version ?? $this->getOwaVersion(); if ( ! $this->getOption( 'owaEndpoint' ) || ! $this->getOption( 'siteId' ) ) { @@ -473,7 +480,7 @@ function isOwaReadyToTrack() { } // 2.x loads .js, so the ID has to be a file name OWA can publish - if ( $this->getOwaVersion() === 2 && ! preg_match( self::BUNDLE_SITE_ID_PATTERN, (string) $this->getOption( 'siteId' ) ) ) { + if ( $version === 2 && ! preg_match( self::BUNDLE_SITE_ID_PATTERN, (string) $this->getOption( 'siteId' ) ) ) { return false; } @@ -483,15 +490,19 @@ function isOwaReadyToTrack() { /** * Credentials for the Website ID list are present. + * + * @param int|null $version 1 or 2; the saved version by default */ - function isOwaSdkReady() { + function isOwaSdkReady( $version = null ) { + + $version = $version ?? $this->getOwaVersion(); if ( ! $this->getOption( 'owaEndpoint' ) ) { return false; } - if ( $this->getOwaVersion() === 2 ) { + if ( $version === 2 ) { return (bool) $this->getOption( 'appPassword' ); } @@ -509,10 +520,6 @@ function initOwaSdk() { if ( ! $this->isOwaSdkReady() ) { - $this->addNag( $this->getOwaVersion() === 2 - ? 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.' - : 'You must enter your OWA API key, Secret Auth key, and Endpoint settings.', 'notice-warning' ); - return; } @@ -712,6 +719,48 @@ function getNetworkNote() { : sprintf( 'The network administrator manages the OWA server (%s) and its credentials. The list shows the Observation Profiles that share this site\'s domain.', $this->getOption( 'owaEndpoint' ) ); } + /** + * What is left to set up before this site is tracked, from the saved + * settings, worded for a version (the saved one by default). + * + * @param int|null $version 1 or 2 + * @return string[] + */ + function setupNotices( $version = null ) { + + $version = $version ?? $this->getOwaVersion(); + $notices = []; + + if ( ! $this->isOwaSdkReady( $version ) ) { + + $notices[] = $version === 2 + ? 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.' + : 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.'; + } + + // where the network sets the Observation Profile, the site admin cannot, and the page says so + $assigned = owaWp\network::isActive() && owaWp\network::options()['assignProfiles']; + + if ( ! $assigned && ! $this->isOwaReadyToTrack( $version ) ) { + + $notices[] = $version === 2 + ? 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' + : 'Open Web Analytics requires that you select a Website ID before tracking can begin.'; + } + + return $notices; + } + + /** + * The site's OWA > Settings screen. + */ + function isSettingsScreen() { + + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; + + return $screen && $screen->id === 'toplevel_page_owa-wordpress'; + } + /** * The plugin's own admin screens. */ diff --git a/src/rest.php b/src/rest.php index 04b4cd1..440f56d 100644 --- a/src/rest.php +++ b/src/rest.php @@ -103,6 +103,8 @@ public static function settingsPayload( $plugin ) { 'secrets' => $secrets, 'sections' => $sections, 'network' => [ 'active' => network::isActive(), 'note' => $plugin->getNetworkNote() ], + // what is left to set up, worded for each version, from the saved settings + 'notices' => [ '1' => $plugin->setupNotices( 1 ), '2' => $plugin->setupNotices( 2 ) ], 'tagSettingsUrl' => $plugin->getOwaVersion() === 2 && $plugin->getOption( 'owaEndpoint' ) && $plugin->getOption( 'siteId' ) ? $plugin->getTagSettingsUrl() : '', ]; } diff --git a/tests/PluginTest.php b/tests/PluginTest.php index 8b04675..b7bb077 100644 --- a/tests/PluginTest.php +++ b/tests/PluginTest.php @@ -75,4 +75,46 @@ public function testNagDefaultsToAWarning(): void { $this->assertStringContainsString( '

Choose a site.

', $out ); $this->assertStringContainsString( '

Broken.

', $out ); } + + public function testSetupNoticesAreWordedForTheVersionAsked(): void { + + $plugin = $this->makePlugin( [ 'owaVersion' => '1' ] ); + + $this->assertSame( [ + 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.', + 'Open Web Analytics requires that you select a Website ID before tracking can begin.', + ], $plugin->setupNotices() ); + + $this->assertSame( [ + 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.', + 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.', + ], $plugin->setupNotices( 2 ) ); + } + + public function testSetupNoticesGoOnceTheSettingsAreSaved(): void { + + $plugin = $this->makePlugin( [ 'owaVersion' => '2', 'owaEndpoint' => 'https://owa.example.com/', 'appPassword' => 'owa_app_x', 'siteId' => 'abc123' ] ); + + $this->assertSame( [], $plugin->setupNotices() ); + $this->assertSame( [ 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.' ], $plugin->setupNotices( 1 ) ); + } + + public function testTheSettingsScreenLeavesSetupNoticesToThePage(): void { + + $plugin = $this->makePlugin(); + $plugin->addNag( 'Broken.', 'notice-error' ); + + Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'toplevel_page_owa-wordpress' ] ); + ob_start(); + $plugin->showNag(); + $settings = ob_get_clean(); + + Functions\when( 'get_current_screen' )->justReturn( (object) [ 'id' => 'dashboard' ] ); + ob_start(); + $plugin->showNag(); + $dashboard = ob_get_clean(); + + $this->assertSame( '

Broken.

', $settings ); + $this->assertStringContainsString( 'select a Website ID before tracking can begin', $dashboard ); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index d435adf..873a4de 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -35,6 +35,7 @@ protected function setUp(): void { 'is_preview' => false, 'is_customize_preview' => false, 'is_multisite' => false, + 'get_current_screen' => fn() => null, ] ); require_once dirname( __DIR__ ) . '/owa-plugin.php'; diff --git a/tests/e2e/single.spec.js b/tests/e2e/single.spec.js index 90e25ce..923dfbd 100644 --- a/tests/e2e/single.spec.js +++ b/tests/e2e/single.spec.js @@ -18,6 +18,10 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await expect( field( 'API Key' ) ).toBeVisible(); await expect( field( 'Application Password' ) ).toHaveCount( 0 ); await expect( field( 'Website ID' ) ).toBeVisible(); + // what is left to set up, worded for 1.x, shown by the page and not again by WordPress + await expect( notice( page, 'Enter your OWA Endpoint, API Key and Secret Auth Key to load the list of websites from OWA.' ) ).toBeVisible(); + await expect( notice( page, 'Open Web Analytics requires that you select a Website ID before tracking can begin.' ) ).toBeVisible(); + await expect( page.locator( 'div.notice-warning' ) ).toHaveCount( 0 ); await shot( page, testInfo, '01-settings-1x' ); await tab( 'Tracking' ).click(); @@ -33,6 +37,10 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' // the same field is the Observation Profile in 2.x await expect( field( 'Website ID' ) ).toHaveCount( 0 ); await expect( field( 'Observation Profile' ).locator( 'option:checked' ) ).toHaveText( /^(Loading|Select Profile)…$/ ); + // the notices switch to 2.x wording with it + await expect( notice( page, 'Enter your OWA Endpoint and an Application Password to load the list of Observation Profiles from OWA.' ) ).toBeVisible(); + await expect( notice( page, 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' ) ).toBeVisible(); + await expect( notice( page, 'API Key and Secret Auth Key' ) ).toHaveCount( 0 ); await shot( page, testInfo, '03-switched-to-2x' ); await tab( 'Tracking' ).click(); @@ -54,6 +62,9 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await save.click(); await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); await expect( page.getByText( 'Saved. Enter a new one to replace it.' ) ).toBeVisible(); + // saved credentials: that notice goes without a reload; choosing an Observation Profile is left + await expect( notice( page, 'to load the list of Observation Profiles from OWA' ) ).toHaveCount( 0 ); + await expect( notice( page, 'Open Web Analytics requires that you select an Observation Profile before tracking can begin.' ) ).toBeVisible(); // the list comes from OWA with the application password await expect( field( 'Observation Profile' ).locator( `option[value="${ siteId }"]` ) ).toHaveCount( 1 ); @@ -65,6 +76,7 @@ test( 'switching to 2.x shows its fields, saves, and tags pages with the bundle' await save.click(); await expect( notice( page, 'Settings saved.' ) ).toBeVisible(); await expect( tagLink ).toBeVisible(); + await expect( notice( page, 'before tracking can begin' ) ).toHaveCount( 0 ); await shot( page, testInfo, '06-2x-saved' ); // choosing another Observation Profile hides it again until that is saved diff --git a/tests/js/fields.test.js b/tests/js/fields.test.js index 0c07a2c..5f7bfdf 100644 --- a/tests/js/fields.test.js +++ b/tests/js/fields.test.js @@ -1,5 +1,5 @@ import { test, expect } from 'vitest'; -import { fieldText, isVisible, showTagSettingsLink, visibleFields, visibleSections } from '../../js/lib/fields'; +import { fieldText, isVisible, setupNotices, showTagSettingsLink, visibleFields, visibleSections } from '../../js/lib/fields'; const FIELDS = [ { key: 'owaVersion', section: 'general', version: null }, @@ -50,3 +50,10 @@ test( 'a field takes its 2.x wording only while 2.x is chosen', () => { expect( fieldText( siteId, '2' ) ).toEqual( { title: 'Observation Profile', description: 'Pick a Profile.' } ); expect( fieldText( { title: 'OWA Endpoint', description: 'The URL.', v2: [] }, '2' ) ).toEqual( { title: 'OWA Endpoint', description: 'The URL.' } ); } ); + +test( 'the setup notices follow the version chosen on the page, and go while tracking is off', () => { + const payload = { notices: { 1: [ 'Enter your API Key.' ], 2: [ 'Enter an Application Password.' ] } }; + expect( setupNotices( payload, { enable: true, owaVersion: '1' } ) ).toEqual( [ 'Enter your API Key.' ] ); + expect( setupNotices( payload, { enable: true, owaVersion: '2' } ) ).toEqual( [ 'Enter an Application Password.' ] ); + expect( setupNotices( payload, { enable: false, owaVersion: '2' } ) ).toEqual( [] ); +} );