diff --git a/inc/class-rest-api.php b/inc/class-rest-api.php index bb65bf2..a28759d 100644 --- a/inc/class-rest-api.php +++ b/inc/class-rest-api.php @@ -7,6 +7,12 @@ namespace Big_Bite\themer; +use WP_Error; +use WP_Theme_JSON; +use WP_REST_Request; +use WP_REST_Response; +use WP_Theme_JSON_Resolver; + /** * Custom REST routes. */ @@ -33,26 +39,53 @@ public function register_routes() { }, ) ); + + register_rest_route( + 'themer/v1', + '/export', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_theme_json' ), + 'permission_callback' => fn() => is_user_logged_in() && current_user_can( 'edit_theme_options' ), + ) + ); } /** * Get custom CSS rules by merging styles from request with existing theme.json data * - * @param \WP_REST_Request $request Full data about the request. - * @return \WP_REST_Response|\WP_Error theme.json generated stylesheet response data or WP_Error on failure. + * @param WP_REST_Request $request Full data about the request. + * @return WP_REST_Response|WP_Error theme.json generated stylesheet response data or WP_Error on failure. */ - public function get_styles( \WP_REST_Request $request ) { - $existing_theme_json = \WP_Theme_JSON_Resolver::get_merged_data(); + public function get_styles( WP_REST_Request $request ) { + $existing_theme_json = WP_Theme_JSON_Resolver::get_merged_data(); - if ( ! $existing_theme_json instanceof \WP_Theme_JSON ) { - return new \WP_Error( 'no_theme_json', __( 'Unable to locate existing theme.json data', 'themer' ) ); + if ( ! $existing_theme_json instanceof WP_Theme_JSON ) { + return new WP_Error( 'no_theme_json', __( 'Unable to locate existing theme.json data', 'themer' ) ); } $custom_styles = $request->get_json_params(); $custom_theme_json_data = array_merge( $existing_theme_json->get_raw_data(), $custom_styles ); - $custom_theme_json = new \WP_Theme_JSON( $custom_theme_json_data ); + $custom_theme_json = new WP_Theme_JSON( $custom_theme_json_data ); return rest_ensure_response( $custom_theme_json->get_stylesheet() ); } + /** + * Returns an updated theme.json with merged and flattened layers + * + * @return WP_REST_Response|WP_Error + */ + public function get_theme_json(): WP_REST_Response | WP_Error { + $all_theme_json_layers = WP_Theme_JSON_Resolver::get_merged_data(); + + if ( ! $all_theme_json_layers instanceof WP_Theme_JSON ) { + return new WP_Error( 'no_theme_json', __( 'Unable to locate existing theme.json data', 'themer' ) ); + } + + $theme_json_raw_data = new WP_Theme_JSON( $all_theme_json_layers->get_raw_data() ); + $theme_json_flattened = $theme_json_raw_data->get_data(); + + return rest_ensure_response( $theme_json_flattened ); + } } diff --git a/src/editor/components/ButtonExport.js b/src/editor/components/ButtonExport.js new file mode 100644 index 0000000..c5fbcad --- /dev/null +++ b/src/editor/components/ButtonExport.js @@ -0,0 +1,62 @@ +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { useState } from '@wordpress/element'; +import { Button } from '@wordpress/components'; + +/** + * Renders the button to export theme.json + */ +const ButtonExport = () => { + const [ isFetching, setIsFetching ] = useState( false ); + const isExportSupported = + window.isSecureContext && 'showSaveFilePicker' in window; + + /** + * Fetch theme JSON object + */ + const fetchThemeJSON = async () => { + setIsFetching( true ); + try { + const response = await apiFetch( { path: '/themer/v1/export' } ); + saveThemeJSON( JSON.stringify( response, null, '\t' ) ); + } catch ( error ) { + console.error( error ); // eslint-disable-line no-console -- Output of caught error + } + setIsFetching( false ); + }; + + /** + * Save JSON blob to a file + * + * @param {Object} data theme.json data + */ + const saveThemeJSON = async ( data ) => { + const blob = new Blob( [ data ], { type: 'application/json' } ); // eslint-disable-line no-undef -- Blob available in browser environment + + const handle = await window.showSaveFilePicker( { + suggestedName: 'theme.json', + } ); + const stream = await handle.createWritable(); + + await stream.write( blob ); + await stream.close(); + }; + + if ( ! isExportSupported ) { + return; + } + + return ( + + ); +}; + +export default ButtonExport; diff --git a/src/editor/components/fields/ThemerComponent.js b/src/editor/components/fields/ThemerComponent.js index 0fb8f1f..882f273 100644 --- a/src/editor/components/fields/ThemerComponent.js +++ b/src/editor/components/fields/ThemerComponent.js @@ -6,6 +6,7 @@ import apiFetch from '@wordpress/api-fetch'; import Preview from './Preview'; import Fields from './Fields'; +import ButtonExport from '../ButtonExport'; /** * main component @@ -123,6 +124,7 @@ const ThemerComponent = () => { onClick={ () => reset() } text="reset to theme.json" /> +