| 1 |
/** |
| 2 |
* WordPress dependencies |
| 3 |
*/ |
| 4 |
import apiFetch from '@wordpress/api-fetch'; |
| 5 |
import { dispatch, useSelect } from '@wordpress/data'; |
| 6 |
import { |
| 7 |
createContext, |
| 8 |
useCallback, |
| 9 |
useContext, |
| 10 |
useEffect, |
| 11 |
useMemo, |
| 12 |
useRef, |
| 13 |
useState, |
| 14 |
} from '@wordpress/element'; |
| 15 |
import type { ReactNode } from 'react'; |
| 16 |
|
| 17 |
/** |
| 18 |
* Internal dependencies |
| 19 |
*/ |
| 20 |
import { SettingsStore } from './store'; |
| 21 |
import { Settings } from './types'; |
| 22 |
|
| 23 |
/** |
| 24 |
* The context type for the settings context. |
| 25 |
* |
| 26 |
* @since 3.14.0 |
| 27 |
*/ |
| 28 |
interface SettingsContextType<T> { |
| 29 |
settings: T; |
| 30 |
setSettings: ( settings: Partial<T> ) => void; |
| 31 |
} |
| 32 |
|
| 33 |
/** |
| 34 |
* The Settings context. |
| 35 |
* |
| 36 |
* @since 3.14.0 |
| 37 |
*/ |
| 38 |
const SettingsContext = createContext<SettingsContextType<Settings>>( { |
| 39 |
settings: {} as Settings, |
| 40 |
setSettings: () => { |
| 41 |
// eslint-disable-next-line no-console |
| 42 |
console.error( 'WP Parse.ly: setSettings not implemented' ); |
| 43 |
}, |
| 44 |
} ); |
| 45 |
|
| 46 |
/** |
| 47 |
* Hook to get the settings from the context. |
| 48 |
* Should only be used within a SettingsProvider. |
| 49 |
* |
| 50 |
* @since 3.14.0 |
| 51 |
* |
| 52 |
* @template T - The type of settings to retrieve, defaults to Settings. |
| 53 |
* |
| 54 |
* @return {SettingsContextType<T>} The settings context. |
| 55 |
* |
| 56 |
* @throws {Error} Throws an error if used outside of a SettingsProvider. |
| 57 |
* |
| 58 |
* @example |
| 59 |
* // Using the useSettings hook with a specific type |
| 60 |
* const { settings, setSettings } = useSettings<SidebarSettings>(); |
| 61 |
*/ |
| 62 |
export function useSettings<T = Settings>(): SettingsContextType<T> { |
| 63 |
const context = useContext( SettingsContext ); |
| 64 |
if ( context === undefined ) { |
| 65 |
throw new Error( 'useSettings must be used within a SettingsProvider' ); |
| 66 |
} |
| 67 |
return context as unknown as SettingsContextType<T>; |
| 68 |
} |
| 69 |
|
| 70 |
/** |
| 71 |
* Custom types for brevity and for avoiding a "type React is undefined" error. |
| 72 |
*/ |
| 73 |
type ReactDeps = React.DependencyList | undefined; |
| 74 |
|
| 75 |
/** |
| 76 |
* Saves the settings into the WordPress database whenever a dependency update |
| 77 |
* occurs. |
| 78 |
* |
| 79 |
* @since 3.13.0 |
| 80 |
* @since 3.14.0 Moved from `content-helper/common/hooks/useSaveSettings.ts`. |
| 81 |
* @since 3.17.0 Updated to the new API endpoints. |
| 82 |
* |
| 83 |
* @param {string} endpoint The settings endpoint to send the data to. |
| 84 |
* @param {Settings} data The data to send. |
| 85 |
* @param {ReactDeps} deps The deps array that triggers saving. |
| 86 |
*/ |
| 87 |
const useSaveSettings = ( |
| 88 |
endpoint: string, data: Settings, deps: ReactDeps |
| 89 |
): void => { |
| 90 |
const isFirstRender = useRef( true ); |
| 91 |
const previousData = useRef<Settings>( data ); |
| 92 |
|
| 93 |
useEffect( () => { |
| 94 |
// Don't save settings on the first render. |
| 95 |
if ( isFirstRender.current ) { |
| 96 |
isFirstRender.current = false; |
| 97 |
previousData.current = data; |
| 98 |
return; |
| 99 |
} |
| 100 |
|
| 101 |
// Only save if the data has actually changed. |
| 102 |
if ( JSON.stringify( previousData.current ) === JSON.stringify( data ) ) { |
| 103 |
return; |
| 104 |
} |
| 105 |
|
| 106 |
previousData.current = data; |
| 107 |
|
| 108 |
apiFetch( { |
| 109 |
path: '/wp-parsely/v2/settings/' + endpoint, |
| 110 |
method: 'PUT', |
| 111 |
data, |
| 112 |
} ); |
| 113 |
}, deps ); // eslint-disable-line react-hooks/exhaustive-deps |
| 114 |
}; |
| 115 |
|
| 116 |
/** |
| 117 |
* The SettingsProvider component properties. |
| 118 |
* |
| 119 |
* @since 3.14.0 |
| 120 |
*/ |
| 121 |
interface SettingsProviderProps { |
| 122 |
children: ReactNode; |
| 123 |
endpoint: string; |
| 124 |
defaultSettings: Settings; |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* The SettingsProvider component. |
| 129 |
* |
| 130 |
* Provides the settings context to its children. |
| 131 |
* It also saves the settings to the WordPress database whenever a setting change occurs. |
| 132 |
* The settings are saved to the WordPress database using the useSaveSettings hook. |
| 133 |
* |
| 134 |
* @since 3.14.0 |
| 135 |
* |
| 136 |
* @param {SettingsProviderProps} props The component's props. |
| 137 |
* |
| 138 |
* @return {import('react').JSX.Element} The SettingsProvider component. |
| 139 |
*/ |
| 140 |
export const SettingsProvider = ( |
| 141 |
{ children, endpoint, defaultSettings }: Readonly<SettingsProviderProps> |
| 142 |
): React.JSX.Element => { |
| 143 |
// Get the current settings from the store. |
| 144 |
const { storedSettings } = useSelect( ( select ) => { |
| 145 |
let settings = select( SettingsStore ).getSettings( endpoint ); |
| 146 |
|
| 147 |
// Set the default settings if empty. |
| 148 |
if ( ! settings ) { |
| 149 |
settings = defaultSettings; |
| 150 |
dispatch( SettingsStore ).setSettings( endpoint, defaultSettings ).then( () => {} ); |
| 151 |
} |
| 152 |
|
| 153 |
return { |
| 154 |
storedSettings: settings, |
| 155 |
}; |
| 156 |
}, [ defaultSettings, endpoint ] ); |
| 157 |
|
| 158 |
// Internal state for storing the settings. |
| 159 |
const [ internalSettings, setInternalSettings ] = useState<Settings>( storedSettings ); |
| 160 |
const { setPartialSettings } = dispatch( SettingsStore ); |
| 161 |
|
| 162 |
/** |
| 163 |
* Updates the settings in the internal state and in the store. |
| 164 |
* |
| 165 |
* @since 3.14.0 |
| 166 |
* |
| 167 |
* @param {Partial<Settings>} updatedSettings The updated settings. |
| 168 |
*/ |
| 169 |
const updateSettings = useCallback( ( updatedSettings: Partial<Settings> ) => { |
| 170 |
setInternalSettings( ( currentSettings ) => ( { ...currentSettings, ...updatedSettings } ) ); |
| 171 |
setPartialSettings( endpoint, updatedSettings ); |
| 172 |
}, [ endpoint, setPartialSettings ] ); |
| 173 |
|
| 174 |
/** |
| 175 |
* Saves the settings into the WordPress database whenever a setting change |
| 176 |
* occurs. |
| 177 |
* |
| 178 |
* internalSettings is the dependency, because we only want to save the settings |
| 179 |
* when they change, and save it with the value in the store (storedSettings). |
| 180 |
* |
| 181 |
* @since 3.14.0 |
| 182 |
*/ |
| 183 |
useSaveSettings( endpoint, storedSettings, [ internalSettings ] ); |
| 184 |
|
| 185 |
// Memoize the provider value to avoid unnecessary re-renders. |
| 186 |
const providerValue = useMemo( () => ( |
| 187 |
{ settings: storedSettings, setSettings: updateSettings } |
| 188 |
), [ storedSettings, updateSettings ] ); |
| 189 |
|
| 190 |
return ( |
| 191 |
<SettingsContext.Provider value={ providerValue }> |
| 192 |
{ children } |
| 193 |
</SettingsContext.Provider> |
| 194 |
); |
| 195 |
}; |
| 196 |
|