PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / pages / settings / shared / SettingsDataProvider.js
presto-player / src / admin / dashboard / pages / settings / shared Last commit date
ConfirmDialog.js 1 month ago LabelWithTooltip.js 1 month ago ProGate.js 1 month ago SectionCard.js 1 month ago SettingsDataProvider.js 1 month ago SettingsPageShell.js 1 month ago UnsavedChangesDialog.js 1 month ago navigationGuard.js 1 month ago
SettingsDataProvider.js
194 lines
1 import {
2 createContext,
3 useContext,
4 useEffect,
5 useRef,
6 useState,
7 useCallback,
8 useMemo,
9 } from 'react';
10 import wpApiFetch from '@wordpress/api-fetch';
11 import isEqual from 'lodash/isEqual';
12
13 const SettingsDataContext = createContext( null );
14
15 export const useSettingsData = () => {
16 const ctx = useContext( SettingsDataContext );
17 if ( ! ctx ) {
18 throw new Error(
19 'useSettingsData must be used inside <SettingsDataProvider>'
20 );
21 }
22 return ctx;
23 };
24
25 const SettingsDataProvider = ( { children } ) => {
26 const [ settings, setSettings ] = useState( {} );
27 const [ lastSaved, setLastSaved ] = useState( {} );
28 const [ isLoading, setIsLoading ] = useState( true );
29 const [ loadError, setLoadError ] = useState( null );
30 const [ savingKeys, setSavingKeys ] = useState( () => new Set() );
31
32 // Mirror state into refs so stable callbacks (saveSlice, resetKey) can read
33 // the freshest values without listing them as deps and losing their identity.
34 const settingsRef = useRef( settings );
35 const lastSavedRef = useRef( lastSaved );
36 useEffect( () => {
37 settingsRef.current = settings;
38 }, [ settings ] );
39 useEffect( () => {
40 lastSavedRef.current = lastSaved;
41 }, [ lastSaved ] );
42
43 // Guards against React 18 StrictMode's double-invoke of mount effects in dev.
44 const hasFetched = useRef( false );
45
46 const fetchSettings = useCallback( async () => {
47 setIsLoading( true );
48 setLoadError( null );
49 try {
50 const response = await wpApiFetch( { path: '/wp/v2/settings' } );
51 setSettings( response );
52 setLastSaved( response );
53 } catch ( err ) {
54 // eslint-disable-next-line no-console
55 console.error( 'SettingsDataProvider: initial fetch failed', err );
56 setLoadError( err );
57 } finally {
58 setIsLoading( false );
59 }
60 }, [] );
61
62 useEffect( () => {
63 if ( hasFetched.current ) {
64 return;
65 }
66 hasFetched.current = true;
67 fetchSettings();
68 }, [ fetchSettings ] );
69
70 const updateLocal = useCallback( ( key, value ) => {
71 setSettings( ( prev ) => {
72 const next = typeof value === 'function' ? value( prev[ key ] ) : value;
73 // Returning the same reference when unchanged avoids a spurious render,
74 // and lets callers' early-return no-ops propagate correctly.
75 if ( isEqual( prev[ key ], next ) ) {
76 return prev;
77 }
78 return { ...prev, [ key ]: next };
79 } );
80 }, [] );
81
82 const resetKey = useCallback( ( key ) => {
83 setSettings( ( prev ) => {
84 const target = lastSavedRef.current[ key ];
85 if ( isEqual( prev[ key ], target ) ) {
86 return prev;
87 }
88 return { ...prev, [ key ]: target };
89 } );
90 }, [] );
91
92 const mutateSaving = useCallback( ( keys, op ) => {
93 setSavingKeys( ( prev ) => {
94 const next = new Set( prev );
95 keys.forEach( ( k ) => next[ op ]( k ) );
96 return next;
97 } );
98 }, [] );
99
100 const saveSlice = useCallback(
101 async ( keys ) => {
102 if ( ! Array.isArray( keys ) || keys.length === 0 ) {
103 return;
104 }
105
106 mutateSaving( keys, 'add' );
107
108 try {
109 const current = settingsRef.current;
110 // Skip null as well as undefined. WP interprets a null in the
111 // request body as "delete/reset this option", and refuses the
112 // call with `rest_invalid_stored_value` whenever the current
113 // stored value doesn't pass the registered schema. A settings
114 // save is never meant to delete, so we never send null.
115 const payload = keys.reduce( ( acc, k ) => {
116 if ( current[ k ] !== undefined && current[ k ] !== null ) {
117 acc[ k ] = current[ k ];
118 }
119 return acc;
120 }, {} );
121
122 if ( Object.keys( payload ).length === 0 ) {
123 return;
124 }
125
126 const response = await wpApiFetch( {
127 path: '/wp/v2/settings',
128 method: 'POST',
129 data: payload,
130 } );
131
132 // WP's POST /wp/v2/settings may legitimately omit keys it considers
133 // unchanged. Fall back to the current draft in that case so we don't
134 // clobber a saved-but-unreturned field with `undefined`.
135 const mergeFromResponse = ( prev ) => {
136 const next = { ...prev };
137 keys.forEach( ( k ) => {
138 if (
139 response &&
140 Object.prototype.hasOwnProperty.call( response, k )
141 ) {
142 next[ k ] = response[ k ];
143 } else if ( current[ k ] !== undefined ) {
144 next[ k ] = current[ k ];
145 }
146 } );
147 return next;
148 };
149
150 setLastSaved( mergeFromResponse );
151 setSettings( mergeFromResponse );
152
153 return response;
154 } finally {
155 mutateSaving( keys, 'delete' );
156 }
157 },
158 [ mutateSaving ]
159 );
160
161 const value = useMemo(
162 () => ( {
163 settings,
164 lastSaved,
165 isLoading,
166 loadError,
167 retryLoad: fetchSettings,
168 savingKeys,
169 updateLocal,
170 resetKey,
171 saveSlice,
172 } ),
173 [
174 settings,
175 lastSaved,
176 isLoading,
177 loadError,
178 fetchSettings,
179 savingKeys,
180 updateLocal,
181 resetKey,
182 saveSlice,
183 ]
184 );
185
186 return (
187 <SettingsDataContext.Provider value={ value }>
188 { children }
189 </SettingsDataContext.Provider>
190 );
191 };
192
193 export default SettingsDataProvider;
194