PluginProbe
CryptX / 4.2.1
CryptX v4.2.1
4.2.1 4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 All 93 releases
cryptx / src / components / App.js

App.js in CryptX 4.2.1, at src/components/App.js

327 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * The screen itself: loads the schema and values, renders the tabs, saves.
3 */
4
5 import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
6 import apiFetch from '@wordpress/api-fetch';
7 import { __ } from '@wordpress/i18n';
8 import {
9 Button,
10 Flex,
11 FlexItem,
12 Notice,
13 Spinner,
14 __experimentalConfirmDialog as ConfirmDialog,
15 } from '@wordpress/components';
16
17 import Tabs from './Tabs';
18 import SettingsTab from './SettingsTab';
19 import HelpTab from './HelpTab';
20
21 const HELP_TAB = 'help';
22
23 /**
24 * Reads the tab from the address so a reload, a bookmark or a shared link all
25 * land on the same tab.
26 *
27 * @param {Array} tabs Known tabs.
28 * @return {string} The tab id to show.
29 */
30 function tabFromLocation( tabs ) {
31 const requested = new URLSearchParams( window.location.search ).get(
32 'tab'
33 );
34 const known = [ ...tabs.map( ( tab ) => tab.id ), HELP_TAB ];
35
36 return known.includes( requested ) ? requested : tabs[ 0 ]?.id;
37 }
38
39 export default function App( { scope = 'site' } ) {
40 const isNetwork = scope === 'network';
41
42 // One route or the other. Everything below is the same screen either way --
43 // the network route hands back a schema without the two settings that mean
44 // something different on every site, so there is no second field list here
45 // that could fall behind the first.
46 const route = isNetwork
47 ? '/cryptx/v1/network-defaults'
48 : '/cryptx/v1/settings';
49
50 const [ schema, setSchema ] = useState( null );
51 const [ values, setValues ] = useState( null );
52 // Only used by the secrets card on the Advanced tab, and read once: a
53 // rotation nobody meant to trigger is otherwise invisible.
54 const [ rotatedAt, setRotatedAt ] = useState( '' );
55 const [ savedValues, setSavedValues ] = useState( null );
56 const [ activeTab, setActiveTab ] = useState( null );
57 const [ notice, setNotice ] = useState( null );
58 const [ isSaving, setSaving ] = useState( false );
59 const [ loadError, setLoadError ] = useState( null );
60 const [ confirmReset, setConfirmReset ] = useState( false );
61
62 useEffect( () => {
63 apiFetch( { path: route } )
64 .then( ( response ) => {
65 setSchema( response.schema );
66 setValues( response.values );
67 setSavedValues( response.values );
68 setRotatedAt( response.secretsRotatedAt || '' );
69 setActiveTab( tabFromLocation( response.schema.tabs ) );
70 } )
71 .catch( ( error ) => {
72 setLoadError(
73 error?.message ||
74 __( 'The settings could not be loaded.', 'cryptx' )
75 );
76 } );
77 }, [ route ] );
78
79 const isDirty = useMemo( () => {
80 if ( ! values || ! savedValues ) {
81 return false;
82 }
83
84 return Object.keys( values ).some(
85 ( key ) => String( values[ key ] ) !== String( savedValues[ key ] )
86 );
87 }, [ values, savedValues ] );
88
89 // Leaving with unsaved changes is the one mistake this screen can make on
90 // the user's behalf, so it is worth the browser's own warning.
91 useEffect( () => {
92 if ( ! isDirty ) {
93 return undefined;
94 }
95
96 const warn = ( event ) => {
97 event.preventDefault();
98 event.returnValue = '';
99 };
100
101 window.addEventListener( 'beforeunload', warn );
102
103 return () => window.removeEventListener( 'beforeunload', warn );
104 }, [ isDirty ] );
105
106 // Keep the browser's back button meaningful: each tab is a history entry.
107 useEffect( () => {
108 const onPopState = () => {
109 if ( schema ) {
110 setActiveTab( tabFromLocation( schema.tabs ) );
111 }
112 };
113
114 window.addEventListener( 'popstate', onPopState );
115
116 return () => window.removeEventListener( 'popstate', onPopState );
117 }, [ schema ] );
118
119 const selectTab = useCallback( ( id ) => {
120 setActiveTab( id );
121
122 const url = new URL( window.location.href );
123 url.searchParams.set( 'tab', id );
124 window.history.pushState( {}, '', url );
125 }, [] );
126
127 const setValue = useCallback( ( key, value ) => {
128 setValues( ( previous ) => ( { ...previous, [ key ]: value } ) );
129 }, [] );
130
131 const save = useCallback( () => {
132 setSaving( true );
133 setNotice( null );
134
135 apiFetch( {
136 path: route,
137 method: 'POST',
138 data: { values },
139 } )
140 .then( ( response ) => {
141 setValues( response.values );
142 setSavedValues( response.values );
143 setNotice( { status: 'success', text: response.message } );
144 } )
145 .catch( ( error ) => {
146 setNotice( {
147 status: 'error',
148 text:
149 error?.message ||
150 __( 'The settings could not be saved.', 'cryptx' ),
151 } );
152 } )
153 .finally( () => setSaving( false ) );
154 }, [ values, route ] );
155
156 const reset = useCallback( () => {
157 setConfirmReset( false );
158 setSaving( true );
159
160 apiFetch( { path: '/cryptx/v1/settings/reset', method: 'POST' } )
161 .then( ( response ) => {
162 setValues( response.values );
163 setSavedValues( response.values );
164 setNotice( { status: 'success', text: response.message } );
165 } )
166 .catch( ( error ) => {
167 setNotice( {
168 status: 'error',
169 text:
170 error?.message ||
171 __( 'The settings could not be reset.', 'cryptx' ),
172 } );
173 } )
174 .finally( () => setSaving( false ) );
175 }, [] );
176
177 if ( loadError ) {
178 return (
179 <Notice status="error" isDismissible={ false }>
180 { loadError }
181 </Notice>
182 );
183 }
184
185 if ( ! schema || ! values ) {
186 return (
187 <div className="cryptx-loading">
188 <Spinner />
189 <span>{ __( 'Loading the settings…', 'cryptx' ) }</span>
190 </div>
191 );
192 }
193
194 const tabs = [
195 ...schema.tabs,
196 {
197 id: HELP_TAB,
198 label: __( 'Help', 'cryptx' ),
199 description: __(
200 'Shortcode, template functions and what changed in each release.',
201 'cryptx'
202 ),
203 },
204 ];
205
206 const current = tabs.find( ( tab ) => tab.id === activeTab ) || tabs[ 0 ];
207
208 return (
209 <div className="cryptx-settings">
210 <header className="cryptx-settings__header">
211 <h1>
212 { isNetwork
213 ? __( 'CryptX network defaults', 'cryptx' )
214 : __( 'CryptX', 'cryptx' ) }
215 </h1>
216 <p className="cryptx-settings__intro">
217 { isNetwork
218 ? __(
219 '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.',
220 'cryptx'
221 )
222 : __(
223 'CryptX hides email addresses in your pages from spam bots while keeping them usable for your visitors.',
224 'cryptx'
225 ) }
226 </p>
227 </header>
228
229 <Tabs tabs={ tabs } active={ current.id } onSelect={ selectTab } />
230
231 { notice && (
232 <Notice
233 status={ notice.status }
234 onRemove={ () => setNotice( null ) }
235 >
236 { notice.text }
237 </Notice>
238 ) }
239
240 <p className="cryptx-settings__tab-description">
241 { current.description }
242 </p>
243
244 <main className="cryptx-settings__body">
245 { current.id === HELP_TAB ? (
246 <HelpTab />
247 ) : (
248 <SettingsTab
249 tab={ current.id }
250 fields={ schema.fields }
251 values={ values }
252 onChange={ setValue }
253 rotatedAt={ rotatedAt }
254 isNetwork={ isNetwork }
255 />
256 ) }
257 </main>
258
259 { current.id !== HELP_TAB && (
260 <div
261 className={
262 'cryptx-settings__actions' +
263 ( isDirty ? ' is-dirty' : '' )
264 }
265 >
266 <Flex justify="space-between" wrap>
267 <FlexItem>
268 <Button
269 variant="primary"
270 onClick={ save }
271 isBusy={ isSaving }
272 disabled={ isSaving || ! isDirty }
273 __next40pxDefaultSize
274 >
275 { __( 'Save changes', 'cryptx' ) }
276 </Button>
277 </FlexItem>
278 { /* Only on the site screen: this route resets the
279 settings of one site, which is not what a network
280 administrator looking at the defaults would
281 expect it to mean. */ }
282 { ! isNetwork && (
283 <FlexItem>
284 <Button
285 variant="tertiary"
286 isDestructive
287 onClick={ () => setConfirmReset( true ) }
288 disabled={ isSaving }
289 __next40pxDefaultSize
290 >
291 { __( 'Restore defaults', 'cryptx' ) }
292 </Button>
293 </FlexItem>
294 ) }
295 </Flex>
296 { /* The state is said in words as well as shown by the
297 button, because a disabled button on its own reads as
298 "broken" rather than as "nothing to do". */ }
299 <p
300 className={
301 'cryptx-settings__status' +
302 ( isDirty ? ' is-dirty' : '' )
303 }
304 aria-live="polite"
305 >
306 { isDirty
307 ? __( 'You have unsaved changes.', 'cryptx' )
308 : __( 'All changes saved.', 'cryptx' ) }
309 </p>
310 </div>
311 ) }
312
313 <ConfirmDialog
314 isOpen={ confirmReset }
315 onConfirm={ reset }
316 onCancel={ () => setConfirmReset( false ) }
317 confirmButtonText={ __( 'Restore defaults', 'cryptx' ) }
318 >
319 { __(
320 'This puts every CryptX setting back to its default, on every tab. Addresses already delivered in cached pages keep working. Continue?',
321 'cryptx'
322 ) }
323 </ConfirmDialog>
324 </div>
325 );
326 }
327