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/fields.js b/js/lib/fields.js index 948e615..67cc667 100644 --- a/js/lib/fields.js +++ b/js/lib/fields.js @@ -16,7 +16,34 @@ 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 }; +} + +/** + * 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 ); } + +/** + * The link to the Website ID's tracking tag screen in OWA shows only for what + * is saved: the server builds it from the saved settings, so it is hidden + * while the version, the endpoint or the Website ID differs from them. + */ +export function showTagSettingsLink( payload, values ) { + return Boolean( payload.tagSettingsUrl ) && + [ 'owaVersion', 'owaEndpoint', 'siteId' ].every( ( key ) => values[ key ] === payload.values[ key ] ); +} diff --git a/js/lib/mapping.js b/js/lib/mapping.js index a01c8b3..1ca000e 100644 --- a/js/lib/mapping.js +++ b/js/lib/mapping.js @@ -2,8 +2,6 @@ * The network settings page's mapping table: each site's Profile. */ -export const CHANGED = 'Changed: saved when you save'; - /** A Profile's option text; one that appeared in OWA since the last save says so. */ export function profileLabel( profile ) { return profile.new ? `${ profile.label } — new` : profile.label; @@ -17,13 +15,34 @@ export function profilesFor( profiles, baseDomain ) { } ); } -/** - * 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. - */ -export function rowStatus( blog, value ) { - const loaded = blog.saved === null ? blog.selected : blog.saved; - return value === loaded ? blog.status : CHANGED; +/** A saved row's Profile, as its cell shows it until Edit is clicked. */ +export function savedLabel( profiles, id ) { + if ( ! id ) { + return 'No Observation Profile'; + } + const profile = profiles.find( ( p ) => p.id === id ); + return profile ? profile.label : `${ id } (not in the list from OWA)`; +} + +/** A row has a saved Profile: it shows it as text with Edit, and a Tracking switch. */ +export function hasSavedProfile( blog ) { + return Boolean( blog.saved ); +} + +/** A row shows its dropdown until a Profile is saved for it, or after Edit. */ +export function isEditing( blog, editing ) { + return ! hasSavedProfile( blog ) || Boolean( editing[ blog.id ] ); +} + +/** The switches to send: only the rows that show one; the server keeps the rest. */ +export function switchesPayload( blogs, switches ) { + const out = {}; + for ( const blog of blogs ) { + if ( hasSavedProfile( blog ) && blog.id in switches ) { + out[ blog.id ] = switches[ blog.id ]; + } + } + return out; } /** The selects' values, as the network route takes them. */ @@ -35,6 +54,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..e5865b5 100644 --- a/js/network/index.js +++ b/js/network/index.js @@ -5,19 +5,19 @@ * 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 { hasSavedProfile, initialMapping, initialSwitches, isEditing, mappingPayload, profileLabel, profilesFor, savedLabel, switchesPayload } 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, 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 ( <> @@ -27,15 +27,15 @@ function SitesTable( { payload, mapping, setMapping } ) { Site - Website ID - { payload.assignProfiles && Status } + Observation Profile + { payload.assignProfiles && Tracking } { payload.blogs.map( ( blog ) => { if ( ! payload.assignProfiles ) { return ( - + { blog.host } { blog.own || 'not chosen yet' } @@ -43,9 +43,17 @@ function SitesTable( { payload, mapping, setMapping } ) { } const value = mapping[ blog.id ] ?? ''; - const status = rowStatus( blog, value ); + // the switch (and greying) only once the row has a saved Profile + const saved = hasSavedProfile( blog ); + const enabled = switches[ blog.id ] ?? true; + const off = saved && ! enabled; + const edit = isEditing( blog, editing ); + + const setValue = ( v ) => setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) ); + const setEdit = ( on ) => setEditing( ( current ) => ( { ...current, [ blog.id ]: on } ) ); + const options = [ - { label: '— not tracked —', value: '' }, + { label: 'Select Profile…', value: '' }, ...profilesFor( payload.profiles, blog.baseDomain ).map( ( profile ) => ( { label: profileLabel( profile ), value: profile.id } ) ), ]; @@ -54,20 +62,54 @@ function SitesTable( { payload, mapping, setMapping } ) { } return ( - - { blog.host } + + + { blog.host } + { blog.saved === null && New } + - setMapping( ( current ) => ( { ...current, [ blog.id ]: v } ) ) } - /> + { edit ? ( +
+ + { saved && ( + + ) } +
+ ) : ( +
+ { savedLabel( payload.profiles, blog.saved ) } + +
+ ) } + + + { saved && ( + setSwitches( ( current ) => ( { ...current, [ blog.id ]: ! enabled } ) ) } + /> + ) } - { status } ); } ) } @@ -83,6 +125,8 @@ function NetworkPage() { const [ assign, setAssign ] = useState( false ); const [ password, setPassword ] = useState( {} ); const [ mapping, setMapping ] = useState( {} ); + const [ switches, setSwitches ] = useState( {} ); + const [ editing, setEditing ] = useState( {} ); const [ errors, setErrors ] = useState( {} ); const [ notices, setNotices ] = useState( [] ); const [ busy, setBusy ] = useState( false ); @@ -94,6 +138,8 @@ function NetworkPage() { setAssign( result.assignProfiles ); setPassword( {} ); setMapping( initialMapping( result.blogs ) ); + setSwitches( initialSwitches( result.blogs ) ); + setEditing( {} ); setErrors( problems ); setNotices( [ ...( result.saved ? [ { status: 'success', text: 'Settings saved.' } ] : [] ), @@ -118,6 +164,7 @@ function NetworkPage() { if ( payload.assignProfiles && assign ) { data.assignedSiteIds = mappingPayload( mapping ); + data.enabledSites = switchesPayload( payload.blogs, switches ); } setBusy( true ); @@ -165,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 || '' } @@ -185,17 +232,27 @@ function NetworkPage() {
- { payload.active && } + { payload.active && ( + + ) }