diff --git a/plugins/woocommerce-admin/.travis.yml b/plugins/woocommerce-admin/.travis.yml index 39c88560ccc..a8304772ebf 100755 --- a/plugins/woocommerce-admin/.travis.yml +++ b/plugins/woocommerce-admin/.travis.yml @@ -49,6 +49,7 @@ script: branches: only: - master + - version/1.0 before_deploy: # Remove our unneeded symlink. diff --git a/plugins/woocommerce-admin/client/analytics/report/index.js b/plugins/woocommerce-admin/client/analytics/report/index.js index eae74f0c8d3..07225b4e4a2 100644 --- a/plugins/woocommerce-admin/client/analytics/report/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/index.js @@ -94,6 +94,18 @@ export const getReports = () => { return applyFilters( REPORTS_FILTER, reports ); }; +/** + * The Customers Report will not have the `report` param supplied by the router/ + * because it no longer exists under the path `/analytics/:report`. Use `props.path`/ + * instead to determine if the Customers Report is being rendered. + * + * @param {Object} props - component props + * @return {string} - report parameter + */ +const getReportParam = ( { params, path } ) => { + return params.report || path.replace( /^\/+/, '' ); +}; + class Report extends Component { constructor() { super( ...arguments ); @@ -117,13 +129,13 @@ class Report extends Component { return null; } - const { params, path, isError } = this.props; + const { isError } = this.props; if ( isError ) { return ; } - const reportParam = params.report || path.replace( /^\/+/, '' ); + const reportParam = getReportParam( this.props ); const report = find( getReports(), { report: reportParam } ); if ( ! report ) { @@ -148,7 +160,7 @@ export default compose( return {}; } - const { report } = props.params; + const report = getReportParam( props ); const searchWords = getSearchWords( query ); // Single Category view in Categories Report uses the products endpoint, so search must also. const mappedReport = diff --git a/plugins/woocommerce-admin/client/analytics/report/products/table.js b/plugins/woocommerce-admin/client/analytics/report/products/table.js index cd19de10753..f6695dfb6b7 100644 --- a/plugins/woocommerce-admin/client/analytics/report/products/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/products/table.js @@ -4,6 +4,7 @@ import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { compose } from '@wordpress/compose'; +import { decodeEntities } from '@wordpress/html-entities'; import { map } from 'lodash'; /** @@ -117,12 +118,13 @@ class ProductsReportTable extends Component { category_ids: categoryIds, low_stock_amount: lowStockAmount, manage_stock: extendedInfoManageStock, - name, sku, stock_status: extendedInfoStockStatus, stock_quantity: stockQuantity, variations = [], } = extendedInfo; + + const name = decodeEntities( extendedInfo.name ); const ordersLink = getNewPath( persistedQuery, '/analytics/orders', @@ -144,7 +146,7 @@ class ProductsReportTable extends Component { const productCategories = ( categoryIds && categoryIds - .map( categoryId => categories.get( categoryId ) ) + .map( ( categoryId ) => categories.get( categoryId ) ) .filter( Boolean ) ) || []; diff --git a/plugins/woocommerce-admin/client/analytics/report/stock/table.js b/plugins/woocommerce-admin/client/analytics/report/stock/table.js index 91f5a039da4..ee5c22afced 100644 --- a/plugins/woocommerce-admin/client/analytics/report/stock/table.js +++ b/plugins/woocommerce-admin/client/analytics/report/stock/table.js @@ -3,6 +3,7 @@ */ import { __, _n, _x } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; /** * WooCommerce dependencies @@ -65,7 +66,6 @@ export default class StockReportTable extends Component { const { id, manage_stock: manageStock, - name, parent_id: parentId, sku, stock_quantity: stockQuantity, @@ -73,6 +73,8 @@ export default class StockReportTable extends Component { low_stock_amount: lowStockAmount, } = product; + const name = decodeEntities( product.name ); + const productDetailLink = getNewPath( persistedQuery, '/analytics/products', diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/industry.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/industry.js index 1d3ea9c696f..876413a8d05 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/industry.js +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/industry.js @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { Component, Fragment } from '@wordpress/element'; import { Button, CheckboxControl } from '@wordpress/components'; import { compose } from '@wordpress/compose'; -import { filter, get, includes } from 'lodash'; +import { filter, get, find, findIndex } from 'lodash'; import { withDispatch } from '@wordpress/data'; /** @@ -16,7 +16,7 @@ import { getSetting } from '@woocommerce/wc-admin-settings'; /** * Internal dependencies */ -import { H, Card } from '@woocommerce/components'; +import { H, Card, TextControl } from '@woocommerce/components'; import withSelect from 'wc-api/with-select'; import { recordEvent } from 'lib/tracks'; @@ -30,9 +30,11 @@ class Industry extends Component { this.state = { error: null, selected: profileItems.industry || [], + textInputListContent: {}, }; this.onContinue = this.onContinue.bind( this ); - this.onChange = this.onChange.bind( this ); + this.onIndustryChange = this.onIndustryChange.bind( this ); + this.onDetailChange = this.onDetailChange.bind( this ); } async onContinue() { @@ -47,9 +49,16 @@ class Industry extends Component { isError, updateProfileItems, } = this.props; + const selectedIndustriesList = this.state.selected.map( + ( industry ) => industry.slug + ); + const industriesWithDetail = filter( this.state.selected, ( value ) => { + return typeof value.detail !== 'undefined'; + } ); recordEvent( 'storeprofiler_store_industry_continue', { - store_industry: this.state.selected, + store_industry: selectedIndustriesList, + industries_with_detail: industriesWithDetail, } ); await updateProfileItems( { industry: this.state.selected } ); @@ -73,19 +82,26 @@ class Industry extends Component { this.setState( { error } ); } - onChange( slug ) { + onIndustryChange( slug ) { this.setState( ( state ) => { - if ( includes( state.selected, slug ) ) { + const newSelected = state.selected; + const selectedIndustry = find( newSelected, { slug } ); + if ( selectedIndustry ) { + const newTextInputListContent = state.textInputListContent; + newTextInputListContent[ slug ] = selectedIndustry.detail; return { selected: filter( state.selected, ( value ) => { - return value !== slug; + return value.slug !== slug; } ) || [], + textInputListContent: newTextInputListContent, }; } - const newSelected = state.selected; - newSelected.push( slug ); + newSelected.push( { + slug, + detail: state.textInputListContent[ slug ], + } ); return { selected: newSelected, }; @@ -94,9 +110,23 @@ class Industry extends Component { ); } + onDetailChange( value, slug ) { + this.setState( ( state ) => { + const newSelected = state.selected; + const newTextInputListContent = state.textInputListContent; + const industryIndex = findIndex( newSelected, { slug } ); + newSelected[ industryIndex ].detail = value; + newTextInputListContent[ slug ] = value; + return { + selected: newSelected, + textInputListContent: newTextInputListContent, + }; + } ); + } + render() { const { industries } = onboarding; - const { error, selected } = this.state; + const { error, selected, textInputListContent } = this.state; return ( @@ -112,14 +142,45 @@ class Industry extends Component {
{ Object.keys( industries ).map( ( slug ) => { + const selectedIndustry = find( selected, { slug } ); + return ( - this.onChange( slug ) } - checked={ selected.includes( slug ) } - className="woocommerce-profile-wizard__checkbox" - /> +
+ + this.onIndustryChange( slug ) + } + checked={ selectedIndustry || false } + className="woocommerce-profile-wizard__checkbox" + /> + { industries[ slug ].use_description && + selectedIndustry && ( + + this.onDetailChange( + value, + selectedIndustry.slug + ) + } + className="woocommerce-profile-wizard__text" + /> + ) } +
); } ) } { error && ( diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/style.scss b/plugins/woocommerce-admin/client/dashboard/profile-wizard/style.scss index 27467c1e446..896d9a73ae3 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/style.scss +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/style.scss @@ -240,16 +240,6 @@ padding: $gap-small $gap; min-height: 56px; - &::after { - content: ''; - position: absolute; - left: $gap-large * 2 + $gap; - width: calc(100% - #{$gap-large * 2 + $gap}); - height: 1px; - background-color: $studio-gray-5; - bottom: 0; - } - .components-base-control { position: relative; } @@ -293,6 +283,10 @@ } } + .woocommerce-profile-wizard__text { + margin: 0 16px 10px; + } + svg.dashicon.components-checkbox-control__checked { left: 1px; top: -1px; diff --git a/plugins/woocommerce-admin/client/dashboard/task-list/index.js b/plugins/woocommerce-admin/client/dashboard/task-list/index.js index 59190bc11c8..b3a21fafc2d 100644 --- a/plugins/woocommerce-admin/client/dashboard/task-list/index.js +++ b/plugins/woocommerce-admin/client/dashboard/task-list/index.js @@ -14,6 +14,7 @@ import { withDispatch } from '@wordpress/data'; */ import { Card, List, MenuItem, EllipsisMenu } from '@woocommerce/components'; import { updateQueryString } from '@woocommerce/navigation'; +import { getSetting } from '@woocommerce/wc-admin-settings'; /** * Internal dependencies @@ -73,8 +74,25 @@ class TaskDashboard extends Component { } ).filter( ( task ) => task.visible ); } + getPluginsInformation() { + const { isJetpackConnected } = this.props; + const { activePlugins, installedPlugins } = getSetting( + 'onboarding', + {} + ); + return { + wcs_installed: installedPlugins.includes( 'woocommerce-services' ), + wcs_active: activePlugins.includes( 'woocommerce-services' ), + jetpack_installed: installedPlugins.includes( 'jetpack' ), + jetpack_active: activePlugins.includes( 'jetpack' ), + jetpack_connected: isJetpackConnected, + }; + } + recordTaskView() { const { task } = this.props.query; + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const pluginsInformation = this.getPluginsInformation(); if ( ! task ) { return; @@ -82,6 +100,7 @@ class TaskDashboard extends Component { recordEvent( 'task_view', { task_name: task, + ...pluginsInformation, } ); } @@ -320,7 +339,9 @@ class TaskDashboard extends Component { export default compose( withSelect( ( select ) => { - const { getProfileItems, getOptions } = select( 'wc-api' ); + const { getProfileItems, getOptions, isJetpackConnected } = select( + 'wc-api' + ); const profileItems = getProfileItems(); const options = getOptions( [ @@ -346,6 +367,7 @@ export default compose( profileItems, promptShown, taskListPayments, + isJetpackConnected: isJetpackConnected(), }; } ), withDispatch( ( dispatch ) => { diff --git a/plugins/woocommerce-admin/client/dashboard/task-list/tasks/appearance.js b/plugins/woocommerce-admin/client/dashboard/task-list/tasks/appearance.js index d9c694064a6..b0432bd1277 100644 --- a/plugins/woocommerce-admin/client/dashboard/task-list/tasks/appearance.js +++ b/plugins/woocommerce-admin/client/dashboard/task-list/tasks/appearance.js @@ -24,9 +24,9 @@ import { getSetting, setSetting } from '@woocommerce/wc-admin-settings'; /** * Internal dependencies */ +import { queueRecordEvent, recordEvent } from 'lib/tracks'; import { WC_ADMIN_NAMESPACE } from 'wc-api/constants'; import withSelect from 'wc-api/with-select'; -import { recordEvent } from 'lib/tracks'; class Appearance extends Component { constructor( props ) { @@ -192,13 +192,25 @@ class Appearance extends Component { path: '/wc-admin/onboarding/tasks/create_homepage', method: 'POST', } ) - .then( ( response ) => { - createNotice( response.status, response.message ); + .then( response => { + createNotice( response.status, response.message, { + actions: response.edit_post_link + ? [ + { + label: __( 'Customize', 'woocommerce-admin' ), + onClick: () => { + queueRecordEvent( 'tasklist_appearance_customize_homepage', {} ); + window.location = `${ + response.edit_post_link + }&wc_onboarding_active_task=homepage`; + }, + }, + ] + : null, + } ); this.setState( { isPending: false } ); - if ( response.edit_post_link ) { - window.location = `${ response.edit_post_link }&wc_onboarding_active_task=homepage`; - } + this.completeStep(); } ) .catch( ( error ) => { createNotice( 'error', error.message ); @@ -284,7 +296,7 @@ class Appearance extends Component { ), content: ( - diff --git a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/utils.js b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/utils.js index 5634acbb8c7..238f9e33344 100644 --- a/plugins/woocommerce-admin/packages/components/src/search/autocompleters/utils.js +++ b/plugins/woocommerce-admin/packages/components/src/search/autocompleters/utils.js @@ -2,6 +2,7 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; /** * @typedef {Object} Completer @@ -24,14 +25,14 @@ export function computeSuggestionMatch( suggestion, query ) { .indexOf( query.toLocaleLowerCase() ); return { - suggestionBeforeMatch: suggestion.substring( 0, indexOfMatch ), - suggestionMatch: suggestion.substring( + suggestionBeforeMatch: decodeEntities( suggestion.substring( 0, indexOfMatch ) ), + suggestionMatch: decodeEntities( suggestion.substring( indexOfMatch, indexOfMatch + query.length - ), - suggestionAfterMatch: suggestion.substring( + ) ), + suggestionAfterMatch: decodeEntities( suggestion.substring( indexOfMatch + query.length - ), + ) ), }; } diff --git a/plugins/woocommerce-admin/packages/components/src/search/index.js b/plugins/woocommerce-admin/packages/components/src/search/index.js index 8535bfb560a..45050331e5f 100644 --- a/plugins/woocommerce-admin/packages/components/src/search/index.js +++ b/plugins/woocommerce-admin/packages/components/src/search/index.js @@ -28,7 +28,7 @@ import { * A search box which autocompletes results while typing, allowing for the user to select an existing object * (product, order, customer, etc). Currently only products are supported. */ -class Search extends Component { +export class Search extends Component { constructor( props ) { super( props ); this.state = { @@ -116,6 +116,10 @@ class Search extends Component { appendFreeTextSearch( options, query ) { const { allowFreeTextSearch } = this.props; + if ( ! query || ! query.length ) { + return []; + } + if ( ! allowFreeTextSearch ) { return options; } diff --git a/plugins/woocommerce-admin/packages/components/src/search/test/index.js b/plugins/woocommerce-admin/packages/components/src/search/test/index.js new file mode 100644 index 00000000000..9be56faf416 --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/search/test/index.js @@ -0,0 +1,42 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { Search } from '../index'; +import { computeSuggestionMatch } from '../autocompleters/utils'; + +describe( 'Search', () => { + it( 'shows the free text search option', () => { + const search = shallow( + + ); + const options = search + .instance() + .appendFreeTextSearch( [], 'Product Query' ); + + expect( options.length ).toBe( 1 ); + } ); + + it( "doesn't show options with an empty search", () => { + const search = shallow( + + ); + const options = search.instance().appendFreeTextSearch( [], '' ); + + expect( options.length ).toBe( 0 ); + } ); + + it( 'returns an object with decoded text', () => { + const decodedText = computeSuggestionMatch( + 'A test & a test', + 'test' + ); + const expected = + '{"suggestionBeforeMatch":"A ","suggestionMatch":"test","suggestionAfterMatch":" & a test"}'; + expect( JSON.stringify( decodedText ) ).toBe( expected ); + } ); +} ); diff --git a/plugins/woocommerce-admin/packages/components/src/select-control/list.js b/plugins/woocommerce-admin/packages/components/src/select-control/list.js index 8dce6fa0e52..734e7993ac7 100644 --- a/plugins/woocommerce-admin/packages/components/src/select-control/list.js +++ b/plugins/woocommerce-admin/packages/components/src/select-control/list.js @@ -154,10 +154,6 @@ class List extends Component { selectedIndex, staticList, } = this.props; - const optionsHaveValues = options ? options[ 0 ].value.id !== '' : false; - if ( ! optionsHaveValues ) { - return null; - } const listboxClasses = classnames( 'woocommerce-select-control__listbox', { diff --git a/plugins/woocommerce-admin/packages/components/src/tag/index.js b/plugins/woocommerce-admin/packages/components/src/tag/index.js index 8eb95cba600..da2be91cdf0 100644 --- a/plugins/woocommerce-admin/packages/components/src/tag/index.js +++ b/plugins/woocommerce-admin/packages/components/src/tag/index.js @@ -5,6 +5,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; import classnames from 'classnames'; import { Button, Dashicon, IconButton, Popover } from '@wordpress/components'; +import { decodeEntities } from '@wordpress/html-entities'; import PropTypes from 'prop-types'; import { withState, withInstanceId } from '@wordpress/compose'; @@ -31,6 +32,7 @@ const Tag = ( { // @todo Maybe this should be a loading indicator? return null; } + label = decodeEntities( label ); const classes = classnames( 'woocommerce-tag', className, { 'has-remove': !! remove, } ); diff --git a/plugins/woocommerce-admin/readme.txt b/plugins/woocommerce-admin/readme.txt index 2af569c933c..dd59cd7e822 100644 --- a/plugins/woocommerce-admin/readme.txt +++ b/plugins/woocommerce-admin/readme.txt @@ -71,6 +71,23 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt == Changelog == += 0.26.0 2020-02-21 = + +- Fix: Warning in product data store when tax amount is non-numeric. #3656 +- Fix: Enable onboarding in production. #3680 +- Enhancement: Move Customers report to WooCommerce Menu #3632 +- Performance: Remove slow physical products query from non setup checklist pages #3722 +- Tweak: use cron instead of Action Scheduler for unsnoozing notes. #3662 +- Dev: Add tracks events when profiler steps are completed #3726 +- Dev: Ensure continue setup loads the onboarding profiler #3646 +- Fix: Added new control in /packages/components/src/select-control/list.js #3700 +- Fix: Alignment of select text #3723 šŸ‘ @edmundcwm +- Performance: Make Stock Panel indicator more performant. #3729 +- Performance: Remove sideloaded images to save on build size #3731 +- Fix: Create Onboarding homepage without redirect #3727 +- Add: Deactivation note for feature plugin #3687 +- Dev: Travis tests on Github for release branch #3751 +======= = 0.25.1 2020-02-07 = - Dev: Enable onboarding #3651 (Onboarding) diff --git a/plugins/woocommerce-admin/src/API/OnboardingProfile.php b/plugins/woocommerce-admin/src/API/OnboardingProfile.php index f1a3a16f88b..6d05f5f64f0 100644 --- a/plugins/woocommerce-admin/src/API/OnboardingProfile.php +++ b/plugins/woocommerce-admin/src/API/OnboardingProfile.php @@ -240,11 +240,9 @@ class OnboardingProfile extends \WC_REST_Data_Controller { 'description' => __( 'Industry.', 'woocommerce-admin' ), 'context' => array( 'view' ), 'readonly' => true, - 'sanitize_callback' => 'wp_parse_slug_list', 'validate_callback' => 'rest_validate_request_arg', 'items' => array( - 'enum' => array_keys( Onboarding::get_allowed_industries() ), - 'type' => 'string', + 'type' => 'json', ), ), 'product_types' => array( diff --git a/plugins/woocommerce-admin/src/API/OnboardingTasks.php b/plugins/woocommerce-admin/src/API/OnboardingTasks.php index 783e101bda9..8c4c2db5b31 100644 --- a/plugins/woocommerce-admin/src/API/OnboardingTasks.php +++ b/plugins/woocommerce-admin/src/API/OnboardingTasks.php @@ -187,13 +187,13 @@ class OnboardingTasks extends \WC_REST_Data_Controller { private static function get_homepage_cover_block( $image ) { $shop_url = get_permalink( wc_get_page_id( 'shop' ) ); if ( ! empty( $image['url'] ) && ! empty( $image['id'] ) ) { - return ' -
+ return ' +

' . __( 'Welcome to the store', 'woocommerce-admin' ) . '

- -

' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '

+ +

' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '

@@ -202,13 +202,13 @@ class OnboardingTasks extends \WC_REST_Data_Controller { '; } - return ' -
-

' . __( 'Welcome to the store', 'woocommerce-admin' ) . '

+ return ' +
+

' . __( 'Welcome to the store', 'woocommerce-admin' ) . '

- -

' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '

+ +

' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '

@@ -319,12 +319,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller { $industry_images = array(); $industries = Onboarding::get_allowed_industries(); foreach ( $industries as $industry_slug => $label ) { - $file_path = WC_ADMIN_ABSPATH . 'images/onboarding/' . $industry_slug . '.jpg'; - if ( 'other' === $industry_slug || ! file_exists( $file_path ) ) { - $industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', plugins_url( 'images/onboarding/other.jpg', WC_ADMIN_PLUGIN_FILE ), $industry_slug ); - continue; - } - $industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', plugins_url( 'images/onboarding/' . $industry_slug . '.jpg', WC_ADMIN_PLUGIN_FILE ), $industry_slug ); + $industry_images[ $industry_slug ] = apply_filters( 'woocommerce_admin_onboarding_industry_image', plugins_url( 'images/onboarding/other-small.jpg', WC_ADMIN_PLUGIN_FILE ), $industry_slug ); } return $industry_images; } @@ -347,14 +342,25 @@ class OnboardingTasks extends \WC_REST_Data_Controller { if ( ! empty( $profile['industry'] ) ) { foreach ( $profile['industry'] as $selected_industry ) { - $images_to_sideload[] = ! empty( $available_images[ $selected_industry ] ) ? $available_images[ $selected_industry ] : $available_images['other']; + if ( is_string( $selected_industry ) ) { + $industry_slug = $selected_industry; + } elseif ( is_array( $selected_industry ) && ! empty( $selected_industry['slug'] ) ) { + $industry_slug = $selected_industry['slug']; + } else { + continue; + } + // Capture the first industry for use in our minimum images logic. + $first_industry = isset( $first_industry ) ? $first_industry : $industry_slug; + $images_to_sideload[] = ! empty( $available_images[ $industry_slug ] ) ? $available_images[ $industry_slug ] : $available_images['other']; } } // Make sure we have at least {$number_of_images} images. if ( count( $images_to_sideload ) < $number_of_images ) { for ( $i = count( $images_to_sideload ); $i < $number_of_images; $i++ ) { - $images_to_sideload[] = ! empty( $profile['industry'] ) && ! empty( $available_images[ $profile['industry'][0] ] ) ? $available_images[ $profile['industry'][0] ] : $available_images['other']; + // Fill up missing image slots with the first selected industry, or other. + $industry = isset( $first_industry ) ? $first_industry : 'other'; + $images_to_sideload[] = empty( $available_images[ $industry ] ) ? $available_images['other'] : $available_images[ $industry ]; } } @@ -402,7 +408,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller { array( 'post_title' => __( 'Homepage', 'woocommerce-admin' ), 'post_type' => 'page', - 'post_status' => 'draft', + 'post_status' => 'publish', 'post_content' => '', // Template content is updated below, so images can be attached to the post. ) ); @@ -417,11 +423,13 @@ class OnboardingTasks extends \WC_REST_Data_Controller { ) ); + update_option( 'show_on_front', 'page' ); + update_option( 'page_on_front', $post_id ); update_option( 'woocommerce_onboarding_homepage_post_id', $post_id ); return array( 'status' => 'success', - 'message' => __( 'Homepage created successfully.', 'woocommerce-admin' ), + 'message' => __( 'Homepage created.', 'woocommerce-admin' ), 'post_id' => $post_id, 'edit_post_link' => htmlspecialchars_decode( get_edit_post_link( $post_id ) ), ); diff --git a/plugins/woocommerce-admin/src/API/Products.php b/plugins/woocommerce-admin/src/API/Products.php index 7b8e867e950..ecc207eccde 100644 --- a/plugins/woocommerce-admin/src/API/Products.php +++ b/plugins/woocommerce-admin/src/API/Products.php @@ -127,6 +127,7 @@ class Products extends \WC_REST_Products_Controller { if ( $request->get_param( 'low_in_stock' ) && is_numeric( $object_data['low_stock_amount'] ) ) { $data->data['low_stock_amount'] = $object_data['low_stock_amount']; } + $data->data['name'] = wp_strip_all_tags( $data->data['name'] ); return $data; } diff --git a/plugins/woocommerce-admin/src/API/Reports/Products/Controller.php b/plugins/woocommerce-admin/src/API/Reports/Products/Controller.php index 6e01cf98cb1..067b5fc80d8 100644 --- a/plugins/woocommerce-admin/src/API/Reports/Products/Controller.php +++ b/plugins/woocommerce-admin/src/API/Reports/Products/Controller.php @@ -70,7 +70,10 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf $data = array(); foreach ( $products_data->data as $product_data ) { - $item = $this->prepare_item_for_response( $product_data, $request ); + $item = $this->prepare_item_for_response( $product_data, $request ); + if ( isset( $item->data['extended_info']['name'] ) ) { + $item->data['extended_info']['name'] = wp_strip_all_tags( $item->data['extended_info']['name'] ); + } $data[] = $this->prepare_response_for_collection( $item ); } diff --git a/plugins/woocommerce-admin/src/API/Reports/Stock/Controller.php b/plugins/woocommerce-admin/src/API/Reports/Stock/Controller.php index 7088fc281d0..99190a47b4b 100644 --- a/plugins/woocommerce-admin/src/API/Reports/Stock/Controller.php +++ b/plugins/woocommerce-admin/src/API/Reports/Stock/Controller.php @@ -290,7 +290,7 @@ class Controller extends \WC_REST_Reports_Controller implements ExportableInterf $data = array( 'id' => $product->get_id(), 'parent_id' => $product->get_parent_id(), - 'name' => $product->get_name(), + 'name' => wp_strip_all_tags( $product->get_name() ), 'sku' => $product->get_sku(), 'stock_status' => $product->get_stock_status(), 'stock_quantity' => (float) $product->get_stock_quantity(), diff --git a/plugins/woocommerce-admin/src/FeaturePlugin.php b/plugins/woocommerce-admin/src/FeaturePlugin.php index 32edccfea1d..98bf7a79a51 100644 --- a/plugins/woocommerce-admin/src/FeaturePlugin.php +++ b/plugins/woocommerce-admin/src/FeaturePlugin.php @@ -148,7 +148,7 @@ class FeaturePlugin { $this->define( 'WC_ADMIN_PLUGIN_FILE', WC_ADMIN_ABSPATH . 'woocommerce-admin.php' ); // WARNING: Do not directly edit this version number constant. // It is updated as part of the prebuild process from the package.json value. - $this->define( 'WC_ADMIN_VERSION_NUMBER', '0.25.1' ); + $this->define( 'WC_ADMIN_VERSION_NUMBER', '0.26.1' ); } /** diff --git a/plugins/woocommerce-admin/src/Features/ActivityPanels.php b/plugins/woocommerce-admin/src/Features/ActivityPanels.php index ffbfa02f588..249b367e8ea 100644 --- a/plugins/woocommerce-admin/src/Features/ActivityPanels.php +++ b/plugins/woocommerce-admin/src/Features/ActivityPanels.php @@ -22,6 +22,11 @@ class ActivityPanels { */ protected static $instance = null; + /** + * Low Stock Transient Name. + */ + const LOW_STOCK_TRANSIENT_NAME = 'woocommerce_admin_low_out_of_stock_count'; + /** * Get class instance. */ @@ -42,6 +47,7 @@ class ActivityPanels { // New settings injection. add_filter( 'woocommerce_shared_settings', array( $this, 'component_settings' ), 20 ); add_action( 'woocommerce_updated', array( $this, 'woocommerce_updated_note' ) ); + add_action( 'woocommerce_update_product', array( __CLASS__, 'clear_low_out_of_stock_count_transient' ) ); } /** @@ -60,13 +66,55 @@ class ActivityPanels { ); } + /** + * Determines if there are out of stock or low stock products. + * + * @return boolean + */ + public function has_low_stock_products() { + global $wpdb; + + // Bail early if store does not manage stock, or Woo version < 3.6 needs lookup tables. + if ( + 'yes' !== get_option( 'woocommerce_manage_stock' ) || + version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) + ) { + return false; + } + + $low_stock_out_of_stock_count = get_transient( self::LOW_STOCK_TRANSIENT_NAME ); + + if ( false === $low_stock_out_of_stock_count ) { + $low_stock_out_of_stock_count = (int) $wpdb->get_var( + "SELECT COUNT( product_id ) + FROM {$wpdb->wc_product_meta_lookup} AS lookup + INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID + WHERE stock_status IN ( 'onbackorder', 'outofstock' ) + AND posts.post_status = 'publish'" + ); + set_transient( self::LOW_STOCK_TRANSIENT_NAME, $low_stock_out_of_stock_count, HOUR_IN_SECONDS ); + } + return $low_stock_out_of_stock_count > 0; + } + + /** + * Clears transient for out of stock indicator + * + * @return boolean + */ + public static function clear_low_out_of_stock_count_transient() { + delete_transient( self::LOW_STOCK_TRANSIENT_NAME ); + return true; + } + /** * Add alert count to the component settings. * * @param array $settings Component settings. */ public function component_settings( $settings ) { - $settings['alertCount'] = WC_Admin_Notes::get_notes_count( array( 'error', 'update' ), array( 'unactioned' ) ); + $settings['alertCount'] = WC_Admin_Notes::get_notes_count( array( 'error', 'update' ), array( 'unactioned' ) ); + $settings['hasLowStock'] = $this->has_low_stock_products(); return $settings; } diff --git a/plugins/woocommerce-admin/src/Features/Onboarding.php b/plugins/woocommerce-admin/src/Features/Onboarding.php index 197cc716a10..f75eda79b7c 100644 --- a/plugins/woocommerce-admin/src/Features/Onboarding.php +++ b/plugins/woocommerce-admin/src/Features/Onboarding.php @@ -182,16 +182,50 @@ class Onboarding { * @return array */ public static function get_allowed_industries() { + /* With "use_description" we turn the description input on. With "description_label" we set the input label */ return apply_filters( 'woocommerce_admin_onboarding_industries', array( - 'fashion-apparel-accessories' => __( 'Fashion, apparel, and accessories', 'woocommerce-admin' ), - 'health-beauty' => __( 'Health and beauty', 'woocommerce-admin' ), - 'art-music-photography' => __( 'Art, music, and photography', 'woocommerce-admin' ), - 'electronics-computers' => __( 'Electronics and computers', 'woocommerce-admin' ), - 'food-drink' => __( 'Food and drink', 'woocommerce-admin' ), - 'home-furniture-garden' => __( 'Home, furniture, and garden', 'woocommerce-admin' ), - 'other' => __( 'Other', 'woocommerce-admin' ), + 'fashion-apparel-accessories' => array( + 'label' => __( 'Fashion, apparel, and accessories', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'health-beauty' => array( + 'label' => __( 'Health and beauty', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'art-music-photography' => array( + 'label' => __( 'Art, music, and photography', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'electronics-computers' => array( + 'label' => __( 'Electronics and computers', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'food-drink' => array( + 'label' => __( 'Food and drink', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'home-furniture-garden' => array( + 'label' => __( 'Home, furniture, and garden', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'cbd-other-hemp-derived-products' => array( + 'label' => __( 'CBD and other hemp-derived products', 'woocommerce-admin' ), + 'use_description' => false, + 'description_label' => '', + ), + 'other' => array( + 'label' => __( 'Other', 'woocommerce-admin' ), + 'use_description' => true, + 'description_label' => 'Description', + ), ) ); } diff --git a/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Deactivate_Plugin.php b/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Deactivate_Plugin.php new file mode 100644 index 00000000000..31ff8e7403c --- /dev/null +++ b/plugins/woocommerce-admin/src/Notes/WC_Admin_Notes_Deactivate_Plugin.php @@ -0,0 +1,82 @@ +get_notes_with_name( self::NOTE_NAME ); + if ( ! empty( $note_ids ) ) { + return; + } + + $note = new WC_Admin_Note(); + $note->set_title( __( 'Deactivate old WooCommerce Admin version', 'woocommerce-admin' ) ); + $note->set_content( __( 'Your current version of WooCommerce Admin is outdated and a newer version is included with WooCommerce. We recommend deactivating the plugin and using the stable version included with WooCommerce.', 'woocommerce-admin' ) ); + $note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_UPDATE ); + $note->set_icon( 'warning' ); + $note->set_name( self::NOTE_NAME ); + $note->set_content_data( (object) array() ); + $note->set_source( 'woocommerce-admin' ); + $note->add_action( + 'deactivate-feature-plugin', + __( 'Deactivate', 'woocommerce-admin' ), + wc_admin_url( '&action=deactivate-feature-plugin' ), + 'unactioned', + true + ); + $note->save(); + } + + /** + * Delete the note if the version is higher than the included. + */ + public static function delete_note() { + WC_Admin_Notes::delete_notes_with_name( self::NOTE_NAME ); + } + + /** + * Deactivate feature plugin. + */ + public function deactivate_feature_plugin() { + /* phpcs:disable WordPress.Security.NonceVerification */ + if ( + ! isset( $_GET['page'] ) || + 'wc-admin' !== $_GET['page'] || + ! isset( $_GET['action'] ) || + 'deactivate-feature-plugin' !== $_GET['action'] || + ! defined( 'WC_ADMIN_PLUGIN_FILE' ) + ) { + return; + } + /* phpcs:enable */ + + $deactivate_url = admin_url( 'plugins.php?action=deactivate&plugin=' . rawurlencode( WC_ADMIN_PLUGIN_FILE ) . '&plugin_status=all&paged=1&_wpnonce=' . wp_create_nonce( 'deactivate-plugin_' . WC_ADMIN_PLUGIN_FILE ) ); + wp_safe_redirect( $deactivate_url ); + exit; + } +} diff --git a/plugins/woocommerce-admin/src/Package.php b/plugins/woocommerce-admin/src/Package.php index 1aeed33b92f..6e2801161cf 100644 --- a/plugins/woocommerce-admin/src/Package.php +++ b/plugins/woocommerce-admin/src/Package.php @@ -5,10 +5,17 @@ * @package Automattic/WooCommerce/WCAdmin */ -namespace Automattic\WooCommerce\Admin; +/** + * This namespace isn't compatible with the PSR-4 + * which ensures that the copy in the standalone plugin will not be autoloaded. + */ +namespace Automattic\WooCommerce\Admin\Composer; defined( 'ABSPATH' ) || exit; +use Automattic\WooCommerce\Admin\Notes\WC_Admin_Notes_Deactivate_Plugin; +use Automattic\WooCommerce\Admin\FeaturePlugin; + /** * Main package class. */ @@ -19,7 +26,14 @@ class Package { * * @var string */ - const VERSION = '0.25.1'; + const VERSION = '0.26.1'; + + /** + * Package active. + * + * @var bool + */ + private static $package_active = false; /** * Init the package. @@ -39,9 +53,20 @@ class Package { // Avoid double initialization when the feature plugin is in use. if ( defined( 'WC_ADMIN_VERSION_NUMBER' ) ) { + $update_version = new WC_Admin_Notes_Deactivate_Plugin(); + if ( version_compare( WC_ADMIN_VERSION_NUMBER, self::VERSION, '<' ) ) { + $update_version::add_note(); + } else { + $update_version::delete_note(); + } + + // Register a deactivation hook for the feature plugin. + register_deactivation_hook( WC_ADMIN_PLUGIN_FILE, array( __CLASS__, 'on_deactivation' ) ); + return; } + self::$package_active = true; FeaturePlugin::instance()->init(); } @@ -54,6 +79,24 @@ class Package { return self::VERSION; } + /** + * Return the active version of WC Admin. + * + * @return string + */ + public static function get_active_version() { + return self::$package_active ? self::VERSION : WC_ADMIN_VERSION_NUMBER; + } + + /** + * Return whether the package is active. + * + * @return bool + */ + public static function is_package_active() { + return self::$package_active; + } + /** * Return the path to the package. * @@ -62,4 +105,12 @@ class Package { public static function get_path() { return dirname( __DIR__ ); } + + /** + * Add deactivation hook for versions of the plugin that don't have the deactivation note. + */ + public static function on_deactivation() { + $update_version = new WC_Admin_Notes_Deactivate_Plugin(); + $update_version::delete_note(); + } } diff --git a/plugins/woocommerce-admin/woocommerce-admin.php b/plugins/woocommerce-admin/woocommerce-admin.php index c64566cb4a0..80669c614d2 100755 --- a/plugins/woocommerce-admin/woocommerce-admin.php +++ b/plugins/woocommerce-admin/woocommerce-admin.php @@ -7,7 +7,7 @@ * Author URI: https://woocommerce.com/ * Text Domain: woocommerce-admin * Domain Path: /languages - * Version: 0.25.1 + * Version: 0.26.1 * Requires at least: 5.3.0 * Requires PHP: 5.6.20 *