/**
* The screen itself: loads the schema and values, renders the tabs, saves.
*/
import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
import {
Button,
Flex,
FlexItem,
Notice,
Spinner,
__experimentalConfirmDialog as ConfirmDialog,
} from '@wordpress/components';
import Tabs from './Tabs';
import SettingsTab from './SettingsTab';
import HelpTab from './HelpTab';
const HELP_TAB = 'help';
/**
* Reads the tab from the address so a reload, a bookmark or a shared link all
* land on the same tab.
*
* @param {Array} tabs Known tabs.
* @return {string} The tab id to show.
*/
function tabFromLocation( tabs ) {
const requested = new URLSearchParams( window.location.search ).get(
'tab'
);
const known = [ ...tabs.map( ( tab ) => tab.id ), HELP_TAB ];
return known.includes( requested ) ? requested : tabs[ 0 ]?.id;
}
export default function App( { scope = 'site' } ) {
const isNetwork = scope === 'network';
// One route or the other. Everything below is the same screen either way --
// the network route hands back a schema without the two settings that mean
// something different on every site, so there is no second field list here
// that could fall behind the first.
const route = isNetwork
? '/cryptx/v1/network-defaults'
: '/cryptx/v1/settings';
const [ schema, setSchema ] = useState( null );
const [ values, setValues ] = useState( null );
// Only used by the secrets card on the Advanced tab, and read once: a
// rotation nobody meant to trigger is otherwise invisible.
const [ rotatedAt, setRotatedAt ] = useState( '' );
const [ savedValues, setSavedValues ] = useState( null );
const [ activeTab, setActiveTab ] = useState( null );
const [ notice, setNotice ] = useState( null );
const [ isSaving, setSaving ] = useState( false );
const [ loadError, setLoadError ] = useState( null );
const [ confirmReset, setConfirmReset ] = useState( false );
useEffect( () => {
apiFetch( { path: route } )
.then( ( response ) => {
setSchema( response.schema );
setValues( response.values );
setSavedValues( response.values );
setRotatedAt( response.secretsRotatedAt || '' );
setActiveTab( tabFromLocation( response.schema.tabs ) );
} )
.catch( ( error ) => {
setLoadError(
error?.message ||
__( 'The settings could not be loaded.', 'cryptx' )
);
} );
}, [ route ] );
const isDirty = useMemo( () => {
if ( ! values || ! savedValues ) {
return false;
}
return Object.keys( values ).some(
( key ) => String( values[ key ] ) !== String( savedValues[ key ] )
);
}, [ values, savedValues ] );
// Leaving with unsaved changes is the one mistake this screen can make on
// the user's behalf, so it is worth the browser's own warning.
useEffect( () => {
if ( ! isDirty ) {
return undefined;
}
const warn = ( event ) => {
event.preventDefault();
event.returnValue = '';
};
window.addEventListener( 'beforeunload', warn );
return () => window.removeEventListener( 'beforeunload', warn );
}, [ isDirty ] );
// Keep the browser's back button meaningful: each tab is a history entry.
useEffect( () => {
const onPopState = () => {
if ( schema ) {
setActiveTab( tabFromLocation( schema.tabs ) );
}
};
window.addEventListener( 'popstate', onPopState );
return () => window.removeEventListener( 'popstate', onPopState );
}, [ schema ] );
const selectTab = useCallback( ( id ) => {
setActiveTab( id );
const url = new URL( window.location.href );
url.searchParams.set( 'tab', id );
window.history.pushState( {}, '', url );
}, [] );
const setValue = useCallback( ( key, value ) => {
setValues( ( previous ) => ( { ...previous, [ key ]: value } ) );
}, [] );
const save = useCallback( () => {
setSaving( true );
setNotice( null );
apiFetch( {
path: route,
method: 'POST',
data: { values },
} )
.then( ( response ) => {
setValues( response.values );
setSavedValues( response.values );
setNotice( { status: 'success', text: response.message } );
} )
.catch( ( error ) => {
setNotice( {
status: 'error',
text:
error?.message ||
__( 'The settings could not be saved.', 'cryptx' ),
} );
} )
.finally( () => setSaving( false ) );
}, [ values, route ] );
const reset = useCallback( () => {
setConfirmReset( false );
setSaving( true );
apiFetch( { path: '/cryptx/v1/settings/reset', method: 'POST' } )
.then( ( response ) => {
setValues( response.values );
setSavedValues( response.values );
setNotice( { status: 'success', text: response.message } );
} )
.catch( ( error ) => {
setNotice( {
status: 'error',
text:
error?.message ||
__( 'The settings could not be reset.', 'cryptx' ),
} );
} )
.finally( () => setSaving( false ) );
}, [] );
if ( loadError ) {
return (
{ isNetwork ? __( 'What a newly created site starts with. Sites that already exist are never changed by this — every site keeps its own settings, and a site administrator can change theirs at any time. Two settings are missing here on purpose: excluded posts and the uploaded image refer to things that exist on one site only. One deserves a second look before you set it: an address on the list under Exceptions is left readable, and a network default puts it on every site created from now on.', 'cryptx' ) : __( 'CryptX hides email addresses in your pages from spam bots while keeping them usable for your visitors.', 'cryptx' ) }
{ current.description }
{ isDirty ? __( 'You have unsaved changes.', 'cryptx' ) : __( 'All changes saved.', 'cryptx' ) }