From 19870c18c0907f2466201c2347563ce4f5b095b9 Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Mon, 8 Jul 2019 10:54:26 +0800 Subject: [PATCH] Onboarding: Add theme uploader component (https://github.com/woocommerce/woocommerce-admin/pull/2580) * Add theme uploader component * Add theme to theme browser on upload complete * Add a minimum height to dropzone area * Use Muriel style Spinner during upload * Add file field uploader to dropzone for accessibility * Delete transient on theme install or theme switch --- .../profile-wizard/steps/theme/index.js | 22 +++- .../profile-wizard/steps/theme/style.scss | 61 +++++++++ .../profile-wizard/steps/theme/uploader.js | 124 ++++++++++++++++++ .../class-wc-admin-rest-themes-controller.php | 10 +- .../onboarding/class-wc-admin-onboarding.php | 33 ++++- 5 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js index 8f50af94ce5..043b71f6968 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/index.js @@ -22,6 +22,7 @@ import { Card, H } from '@woocommerce/components'; import withSelect from 'wc-api/with-select'; import './style.scss'; import { recordEvent } from 'lib/tracks'; +import ThemeUploader from './uploader'; class Theme extends Component { constructor() { @@ -29,8 +30,10 @@ class Theme extends Component { this.state = { activeTab: 'all', + uploadedThemes: [], }; + this.handleUploadComplete = this.handleUploadComplete.bind( this ); this.onChoose = this.onChoose.bind( this ); this.onSelectTab = this.onSelectTab.bind( this ); this.openDemo = this.openDemo.bind( this ); @@ -128,17 +131,27 @@ class Theme extends Component { } getThemes() { + const { activeTab, uploadedThemes } = this.state; const { themes } = wcSettings.onboarding; - const { activeTab } = this.state; + themes.concat( uploadedThemes ); + const allThemes = [ ...themes, ...uploadedThemes ]; switch ( activeTab ) { case 'paid': - return themes.filter( theme => this.getPriceValue( theme.price ) > 0 ); + return allThemes.filter( theme => this.getPriceValue( theme.price ) > 0 ); case 'free': - return themes.filter( theme => this.getPriceValue( theme.price ) <= 0 ); + return allThemes.filter( theme => this.getPriceValue( theme.price ) <= 0 ); case 'all': default: - return themes; + return allThemes; + } + } + + handleUploadComplete( upload ) { + if ( 'success' === upload.status && upload.theme_data ) { + this.setState( { + uploadedThemes: [ ...this.state.uploadedThemes, upload.theme_data ], + } ); } } @@ -175,6 +188,7 @@ class Theme extends Component { { () => (
{ themes && themes.map( theme => this.renderTheme( theme ) ) } +
) } diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss index ffcb07f7e36..9686dfe0ecb 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/style.scss @@ -116,3 +116,64 @@ } } } + +.woocommerce-profile-wizard__body .woocommerce-theme-uploader.woocommerce-card { + margin: 0; + position: relative; + + .woocommerce-card__body { + height: 100%; + } + + &.is-uploading .components-drop-zone__provider { + min-height: 382px; + } + + .components-drop-zone__provider { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 2px; + background: #f6f6f6; + border: 1px dashed #b0b5b8; + } + + .components-form-file-upload { + flex: 1; + width: 100%; + } + + .components-form-file-upload > .components-button { + flex: 1; + flex-direction: column; + margin: 0; + width: 100%; + height: 100%; + min-height: 380px; + + > .gridicon { + width: 48px; + height: 48px; + + path { + fill: #50575d; + } + } + + .dashicons-upload { + display: none; + } + } + + .woocommerce-theme-uploader__title { + margin: $gap-smaller 0; + @include font-size(24); + font-weight: 400; + } + + p { + @include font-size(14); + margin: 0; + } +} diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js new file mode 100644 index 00000000000..a7f0446d5c7 --- /dev/null +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/theme/uploader.js @@ -0,0 +1,124 @@ +/** @format */ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import classnames from 'classnames'; +import { Component, Fragment } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { DropZoneProvider, DropZone, FormFileUpload } from '@wordpress/components'; +import Gridicon from 'gridicons'; +import { noop } from 'lodash'; +import PropTypes from 'prop-types'; +import { withDispatch } from '@wordpress/data'; + +/** + * WooCommerce dependencies + */ +import { Card, H, Spinner } from '@woocommerce/components'; + +class ThemeUploader extends Component { + constructor() { + super(); + + this.state = { + isUploading: false, + }; + + this.handleFilesUpload = this.handleFilesUpload.bind( this ); + this.handleFilesDrop = this.handleFilesDrop.bind( this ); + } + + handleFilesDrop( files ) { + const file = files[ 0 ]; + this.uploadTheme( file ); + } + + handleFilesUpload( e ) { + const file = e.target.files[ 0 ]; + this.uploadTheme( file ); + } + + uploadTheme( file ) { + const { addNotice, onUploadComplete } = this.props; + this.setState( { isUploading: true } ); + + const body = new FormData(); + body.append( 'pluginzip', file ); + + return apiFetch( { path: '/wc-admin/v1/themes', method: 'POST', body } ) + .then( response => { + onUploadComplete( response ); + this.setState( { isUploading: false } ); + addNotice( { status: response.status, message: response.message } ); + } ) + .catch( error => { + this.setState( { isUploading: false } ); + if ( error && error.message ) { + addNotice( { status: 'error', message: error.message } ); + } + } ); + } + + render() { + const { className } = this.props; + const { isUploading } = this.state; + + const classes = classnames( 'woocommerce-theme-uploader', className, { + 'is-uploading': isUploading, + } ); + + return ( + + + { ! isUploading ? ( + + + + + { __( 'Upload a theme', 'woocommerce-admin' ) } + +

{ __( 'Drop a theme zip file here to upload', 'woocommerce-admin' ) }

+
+ +
+ ) : ( + + + + { __( 'Uploading theme', 'woocommerce-admin' ) } + +

{ __( 'Your theme is being uploaded', 'woocommerce-admin' ) }

+
+ ) } +
+
+ ); + } +} + +ThemeUploader.propTypes = { + /** + * Additional class name to style the component. + */ + className: PropTypes.string, + /** + * Function called when an upload has finished. + */ + onUploadComplete: PropTypes.func, +}; + +ThemeUploader.defaultProps = { + onUploadComplete: noop, +}; + +export default compose( + withDispatch( dispatch => { + const { addNotice } = dispatch( 'wc-admin' ); + return { addNotice }; + } ) +)( ThemeUploader ); diff --git a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php index 8584c48ff6c..df7782cfcb7 100644 --- a/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php +++ b/plugins/woocommerce-admin/includes/api/class-wc-admin-rest-themes-controller.php @@ -89,11 +89,19 @@ class WC_Admin_REST_Themes_Controller extends WC_REST_Data_Controller { } if ( ! is_wp_error( $install ) && isset( $install['destination_name'] ) ) { + $theme = $install['destination_name']; $result = array( 'status' => 'success', 'message' => $upgrader->strings['process_success'], - 'theme' => $install['destination_name'], + 'theme' => $theme, ); + + /** + * Fires when a theme is successfully installed. + * + * @param string $theme The theme name. + */ + do_action( 'woocommerce_theme_installed', $theme ); } else { if ( is_wp_error( $install ) && $install->get_error_code() ) { $error_message = isset( $upgrader->strings[ $install->get_error_code() ] ) ? $upgrader->strings[ $install->get_error_code() ] : $install->get_error_data(); diff --git a/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php b/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php index 64aa6548900..56de15f8395 100644 --- a/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php +++ b/plugins/woocommerce-admin/includes/features/onboarding/class-wc-admin-onboarding.php @@ -17,6 +17,20 @@ class WC_Admin_Onboarding { */ protected static $instance = null; + /** + * Name of themes transient. + * + * @var string + */ + const THEMES_TRANSIENT = 'wc_onboarding_themes'; + + /** + * Name of product data transient. + * + * @var string + */ + const PRODUCT_DATA_TRANSIENT = 'wc_onboarding_product_data'; + /** * Get class instance. */ @@ -32,6 +46,8 @@ class WC_Admin_Onboarding { */ public function __construct() { add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // Run after WC_Admin_Loader. + add_action( 'woocommerce_theme_installed', array( $this, 'delete_themes_transient' ) ); + add_action( 'after_switch_theme', array( $this, 'delete_themes_transient' ) ); add_filter( 'woocommerce_admin_is_loading', array( $this, 'is_loading' ) ); add_filter( 'woocommerce_rest_prepare_themes', array( $this, 'add_uploaded_theme_data' ) ); } @@ -116,8 +132,7 @@ class WC_Admin_Onboarding { * @return array */ public static function get_themes() { - $themes_transient_name = 'wc_onboarding_themes'; - $themes = get_transient( $themes_transient_name ); + $themes = get_transient( self::THEMES_TRANSIENT ); if ( false === $themes ) { $theme_data = wp_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=themes' ); $themes = array(); @@ -152,7 +167,7 @@ class WC_Admin_Onboarding { $themes = array( $active_theme => $themes[ $active_theme ] ) + $themes; - set_transient( $themes_transient_name, $themes, DAY_IN_SECONDS ); + set_transient( self::THEMES_TRANSIENT, $themes, DAY_IN_SECONDS ); } $themes = apply_filters( 'woocommerce_admin_onboarding_themes', $themes ); @@ -221,15 +236,14 @@ class WC_Admin_Onboarding { * @return array */ public static function append_product_data( $product_types ) { - $product_data_transient_name = 'wc_onboarding_product_data'; - $woocommerce_products = get_transient( $product_data_transient_name ); + $woocommerce_products = get_transient( self::PRODUCT_DATA_TRANSIENT ); if ( false === $woocommerce_products ) { $woocommerce_products = wp_remote_get( 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search?category=product-type' ); if ( is_wp_error( $woocommerce_products ) ) { return $product_types; } - set_transient( $product_data_transient_name, $woocommerce_products, DAY_IN_SECONDS ); + set_transient( self::PRODUCT_DATA_TRANSIENT, $woocommerce_products, DAY_IN_SECONDS ); } $product_data = json_decode( $woocommerce_products['body'] ); @@ -253,6 +267,13 @@ class WC_Admin_Onboarding { return $product_types; } + /** + * Delete the stored themes transient. + */ + public static function delete_themes_transient() { + delete_transient( self::THEMES_TRANSIENT ); + } + /** * Add profiler items to component settings. *