Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Export (Clone) to site editor #292

Merged
merged 11 commits into from
Apr 3, 2023
41 changes: 39 additions & 2 deletions admin/class-create-block-theme-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ class Create_Block_Theme_Admin {
public function __construct() {
add_action( 'admin_menu', array( $this, 'create_admin_menu' ) );
add_action( 'admin_init', array( $this, 'blockbase_save_theme' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'create_block_theme_enqueue' ) );
add_action( 'rest_api_init', array( $this, 'register_theme_export' ) );
}

function create_block_theme_enqueue() {
$asset_file = include( plugin_dir_path( dirname( __FILE__ ) ) . 'build/plugin-sidebar.asset.php' );

wp_register_script(
'create-block-theme-slot-fill',
plugins_url( 'build/plugin-sidebar.js', dirname( __FILE__ ) ),
$asset_file['dependencies'],
$asset_file['version']
);
wp_enqueue_script(
'create-block-theme-slot-fill',
);
}

function create_admin_menu() {
Expand Down Expand Up @@ -187,9 +203,30 @@ function clone_theme( $theme, $screenshot ) {
header( 'Content-Disposition: attachment; filename=' . $theme['slug'] . '.zip' );
header( 'Content-Length: ' . filesize( $filename ) );
flush();
echo readfile( $filename );
die();
readfile( $filename );
unlink( $filename );
exit;
}

function rest_export_theme( $request ) {
$theme = $request->get_params();
$this->clone_theme( $theme, null );
}

public function register_theme_export() {
register_rest_route(
'create-block-theme/v1',
'/export',
array(
'methods' => 'POST',
'callback' => array( $this, 'rest_export_theme' ),
'permission_callback' => function () {
return current_user_can( 'edit_theme_options' );
},
)
);
}

/**
* Create a child theme of the activated theme
*/
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"simple-git": "^3.14.1"
},
"scripts": {
"build": "wp-scripts build",
"build": "wp-scripts build src/index.js src/plugin-sidebar.js",
"format": "wp-scripts format",
"lint:css": "wp-scripts lint-style",
"lint:css:fix": "npm run lint:css -- --fix",
Expand All @@ -47,7 +47,7 @@
"lint:php": "composer run-script lint",
"lint:php:fix": "composer run-script format",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start",
"start": "wp-scripts start src/index.js src/plugin-sidebar.js",
"update-version": "node update-version-and-changelog.js",
"prepare": "husky install"
},
Expand Down
182 changes: 182 additions & 0 deletions src/plugin-sidebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { registerPlugin } from '@wordpress/plugins';
import { PluginSidebar, PluginSidebarMoreMenuItem } from '@wordpress/edit-site';
import { tool } from '@wordpress/icons';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import {
Button,
// eslint-disable-next-line
__experimentalVStack as VStack,
// eslint-disable-next-line
__experimentalSpacer as Spacer,
// eslint-disable-next-line
__experimentalText as Text,
// eslint-disable-next-line
__experimentalHeading as Heading,
PanelBody,
TextControl,
} from '@wordpress/components';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch, useSelect } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';

import { downloadFile } from './utils';

const ExportTheme = () => {
const { createErrorNotice } = useDispatch( noticesStore );
const [ theme, setTheme ] = useState( {
name: '',
description: '',
uri: '',
author: '',
author_uri: '',
} );

useSelect( ( select ) => {
const themeData = select( 'core' ).getCurrentTheme();
setTheme( {
name: themeData.name.raw,
description: themeData.description.raw,
author: themeData.author.raw,
author_uri: themeData.author_uri.raw,
theme_uri: themeData.theme_uri.raw,
} );
}, [] );

const handleSubmit = () => {
const fetchOptions = {
path: '/create-block-theme/v1/export',
method: 'POST',
data: theme,
headers: {
'Content-Type': 'application/json',
},
parse: false,
};

async function exportTheme() {
try {
const response = await apiFetch( fetchOptions );
downloadFile( response );
} catch ( error ) {
const errorMessage =
error.message && error.code !== 'unknown_error'
? error.message
: __(
'An error occurred while attempting to export the theme.'
);
createErrorNotice( errorMessage, { type: 'snackbar' } );
}
}

exportTheme();
};

return (
<PanelBody>
<Heading>{ __( 'Export', 'create-block-theme' ) }</Heading>
<VStack>
<Text variant="muted">
{ __(
'Export your theme with updated templates and styles.',
'create-block-theme'
) }
</Text>
<Spacer />
<TextControl
label={ __( 'Theme name', 'create-block-theme' ) }
value={ theme.name }
onChange={ ( value ) =>
setTheme( { ...theme, name: value } )
}
placeholder={ __( 'Theme name', 'create-block-theme' ) }
/>
<TextControl
label={ __( 'Theme description', 'create-block-theme' ) }
value={ theme.description }
onChange={ ( value ) =>
setTheme( { ...theme, description: value } )
}
placeholder={ __(
'A short description of the theme',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Theme URI', 'create-block-theme' ) }
value={ theme.uri }
onChange={ ( value ) =>
setTheme( { ...theme, uri: value } )
}
placeholder={ __(
'https://github.com/wordpress/twentytwentythree/',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Author', 'create-block-theme' ) }
value={ theme.author }
onChange={ ( value ) =>
setTheme( { ...theme, author: value } )
}
placeholder={ __(
'the WordPress team',
'create-block-theme'
) }
/>
<TextControl
label={ __( 'Author URI', 'create-block-theme' ) }
value={ theme.author_uri }
onChange={ ( value ) =>
setTheme( { ...theme, author_uri: value } )
}
placeholder={ __(
'https://wordpress.org/',
'create-block-theme'
) }
/>
</VStack>
<Spacer />
<Button
variant="secondary"
disabled={ ! theme.name }
onClick={ handleSubmit }
>
{ __( 'Export', 'create-block-theme' ) }
</Button>
<Spacer />
{ ! theme.name && (
<Text variant="muted">
{ __(
'Theme name is required for export.',
'create-block-theme'
) }
</Text>
) }
</PanelBody>
);
};

const CreateBlockThemePlugin = () => {
return (
<>
<PluginSidebarMoreMenuItem
target="create-block-theme-sidebar"
icon={ tool }
>
{ __( 'Create Block Theme' ) }
</PluginSidebarMoreMenuItem>
<PluginSidebar
name="create-block-theme-sidebar"
icon={ tool }
title={ __( 'Create Block Theme' ) }
>
<ExportTheme />
</PluginSidebar>
</>
);
};

registerPlugin( 'cbt-plugin-sidebar', {
render: CreateBlockThemePlugin,
} );
27 changes: 27 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,30 @@ export function localFileAsThemeAssetUrl( url ) {
}
return url.replace( 'file:./', createBlockTheme.themeUrl + '/' );
}

export async function downloadFile( response ) {
const blob = await response.blob();
const filename = response.headers
.get( 'Content-Disposition' )
.split( 'filename=' )[ 1 ];

// Check if the browser supports navigator.msSaveBlob or navigator.saveBlob
if ( navigator.msSaveBlob || navigator.saveBlob ) {
const saveBlob = navigator.msSaveBlob || navigator.saveBlob;
saveBlob.call( navigator, blob, filename );
} else {
// Fall back to creating an object URL and triggering a download using an anchor element
const url = URL.createObjectURL( blob );

const a = document.createElement( 'a' );
a.href = url;
a.download = filename;
document.body.appendChild( a );
a.click();
document.body.removeChild( a );

setTimeout( () => {
URL.revokeObjectURL( url );
}, 100 );
}
}