* Try: Moving Customers to main Woo Menu (https://github.com/woocommerce/woocommerce-admin/pull/3632)

* Only add onboarding settings on wc-admin pages when task list should be shown. (https://github.com/woocommerce/woocommerce-admin/pull/3722)

* Use cron for unsnoozing admin notes (https://github.com/woocommerce/woocommerce-admin/pull/3662)

* Use wp-cron for admin note snoozing.

* Remove "unsnooze" scheduled action.

* Use correct version.

* Avoid using deprecated method for unscheduling actions.

* Onboarding: Fix toggle tracking events (https://github.com/woocommerce/woocommerce-admin/pull/3645)

* Fix errant wcadmin prefix on event name

* Track the onboarding toggle on the option in case enable_onboarding isn't used

* Move toggle actions to separate function

* Move onboarding actions

* Move onboarding filters

* Move help tab updates to add_toggle_actions

* Only run onboarding actions when enabled

* Onboarding: Add tracks events when profiler steps are completed (https://github.com/woocommerce/woocommerce-admin/pull/3726)

* Add tracks for store profiler step completion

* Record event when profiler is completed

* Ensure continue setup loads the onboarding profiler (https://github.com/woocommerce/woocommerce-admin/pull/3646)

* 'All that include' option removed when input field is empty (https://github.com/woocommerce/woocommerce-admin/pull/3700)

* 'All that include' option removed when input field is empty

Added a control to check that when the input field 'Search by customer name' is empty, the 'All that include' option is not appearing.

* Const name improved

The constant name hasValues was changed to optionsHaveValues (more descriptive)

* Fix select text alignment (https://github.com/woocommerce/woocommerce-admin/pull/3723)

* Stock panel indicator - cache and use lookup tables. (https://github.com/woocommerce/woocommerce-admin/pull/3729)

* Stock panel indicator - cache and use lookup tables.

* Revise query, clear transient on product update.

* Fix error, ht Josh.

* Checklist: Remove sideloaded images to reduce build size, take 2 (https://github.com/woocommerce/woocommerce-admin/pull/3731)

* Remove homepage template images.

* Use other-small on all industries, adjust text color.

* Remove background dim and opacity set to 0

* Fix/3631 (https://github.com/woocommerce/woocommerce-admin/pull/3730)

* Added CBD as an industry type

CBD was added as an industry type in API

* Industries options modified

Modified the industries options. Now we are able to choose if we will use an input or not in the option.

* API control changed for industries.

API control changed for industries. Now it accepts the data type we need.

* Added input in Industries list for the option "Other"

Added an input for the option "Other" in the industries list

* Added suggested changes in review comments.

* Added data preparation for recordEvent

* Changed variable to snake_case

The variable "industriesWithDetail" was changed to "industries_with_detail" (snake_case)

* Onboarding: Create homepage without redirect (https://github.com/woocommerce/woocommerce-admin/pull/3727)

* Add link to edit homepage instead of redirect

* Add busy state to homepage creation button

* Publish homepage on create via API

* Update homepage notice to show on first post update

* Update homepage creation notice per design

* Record event on customize homepage

* Set homepage to frontpage on creation

* Add deactivation note for feature plugin (https://github.com/woocommerce/woocommerce-admin/pull/3687)

* Add version deactivation note

* Add the note to deactivate if the version is older than the current WC version

* Deactivate wc admin feature plugin on action click

* Add notes version hooks

* change the Package class namespace to exclude from standalone autoloader

* add use statement for FeaturePlugin

* add note explaining namespace

* use wc-admin-deactivate-plugin note name

* Rename file and class to WC_Admin_Notes_Deactivate_Plugin

Co-authored-by: Ron Rennick <ron@ronandandrea.com>
Co-authored-by: Paul Sealock <psealock@gmail.com>

* Add Travis tests on GH for release branch (https://github.com/woocommerce/woocommerce-admin/pull/3751)

* Add Travis tests on GH for release branch

* fix linter errors

* ActivityPanels.php -> use public static functions

* Remove free text Search option when no query exists (https://github.com/woocommerce/woocommerce-admin/pull/3755)

* Revert changes in woocommerce/woocommerce-admin#3700

* Don't add free text search if no query exists

* Add tests for Search without query

* Add test for showing free text search option

* Fix image sideloading for store industries. (https://github.com/woocommerce/woocommerce-admin/pull/3743)

* Fix image sideloading for store industries.

Data format changed in https://github.com/woocommerce/woocommerce-admin/pull/3730

* Fix industry image sideload in cases where the count is less than requested.

* Be backwards compatible with the old industry data format.

* Added event props to identify stores with WCS and Jetpack installed (https://github.com/woocommerce/woocommerce-admin/pull/3750)

* Added event props to identify stores with WCS and Jetpack installed

Also, added Jeckpack connected status

* Improved variable name

* Simplified method

Simplified method. "intersection" check was removed

* Tests errors repeared

The method "clear_low_out_of_stock_count_transient" now is static.

* OBW: fix sideloading image test error (https://github.com/woocommerce/woocommerce-admin/pull/3762)

* Release 0.26.0 changes (https://github.com/woocommerce/woocommerce-admin/pull/3753)

* add deactivation hook to Package.php (https://github.com/woocommerce/woocommerce-admin/pull/3770)

* Add active version functions (https://github.com/woocommerce/woocommerce-admin/pull/3772)

* add active version functions to Package.php

Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>

* 0.26.1 changes (https://github.com/woocommerce/woocommerce-admin/pull/3773)

* Customers Report: fix missing report param in search (https://github.com/woocommerce/woocommerce-admin/pull/3778)

* Product titles include encoded entities (https://github.com/woocommerce/woocommerce-admin/pull/3765)

* Stripped HTML from product titles and decoded before displaying them

Stripped html from product titles and entities are decoded before displaying them

* Stripped HTML from product titles and decoded in Stock report

Stripped html from product titles and entities are decoded before displaying them. Now in Stock report

* Added support for HTML tags and encoded entities on product titles

Added support for HTML tags and encoded entities on filtered product list, dropdown menus and tag names.
Also, strip_tags() function was replaced with wp_strip_all_tags() instead.

* strip_tags() function was replaced with wp_strip_all_tags() instead.

* Added control for a variable

Added control for "item->data" before applying wp_strip_all_tags method.

* pre-commit changes

* Test text corrected

* fix linting issues

* fix mis-merged changes

* Update jsdoc

Co-Authored-By: Paul Sealock <psealock@gmail.com>

Co-authored-by: Timmy Crawford <timmyc@users.noreply.github.com>
Co-authored-by: Jeff Stieler <jeff.m.stieler@gmail.com>
Co-authored-by: Joshua T Flowers <joshuatf@gmail.com>
Co-authored-by: Fernando <ultimoround@gmail.com>
Co-authored-by: edmundcwm <edmundcwm@gmail.com>
Co-authored-by: Paul Sealock <psealock@gmail.com>
This commit is contained in:
Ron Rennick 2020-03-02 18:22:32 -04:00 committed by GitHub
parent 83ff92910a
commit 54d58ed13e
39 changed files with 500 additions and 135 deletions

View File

@ -49,6 +49,7 @@ script:
branches:
only:
- master
- version/1.0
before_deploy:
# Remove our unneeded symlink.

View File

@ -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 <ReportError isError />;
}
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 =

View File

@ -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 ) ) ||
[];

View File

@ -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',

View File

@ -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 (
<Fragment>
@ -112,14 +142,45 @@ class Industry extends Component {
<Card>
<div className="woocommerce-profile-wizard__checkbox-group">
{ Object.keys( industries ).map( ( slug ) => {
const selectedIndustry = find( selected, { slug } );
return (
<CheckboxControl
key={ slug }
label={ industries[ slug ] }
onChange={ () => this.onChange( slug ) }
checked={ selected.includes( slug ) }
className="woocommerce-profile-wizard__checkbox"
/>
<div key={ `div-${ slug }` }>
<CheckboxControl
key={ `checkbox-control-${ slug }` }
label={ industries[ slug ].label }
onChange={ () =>
this.onIndustryChange( slug )
}
checked={ selectedIndustry || false }
className="woocommerce-profile-wizard__checkbox"
/>
{ industries[ slug ].use_description &&
selectedIndustry && (
<TextControl
key={ `text-control-${ selectedIndustry.slug }` }
label={
industries[
selectedIndustry.slug
].description_label
}
value={
selectedIndustry.detail ||
textInputListContent[
slug
] ||
''
}
onChange={ ( value ) =>
this.onDetailChange(
value,
selectedIndustry.slug
)
}
className="woocommerce-profile-wizard__text"
/>
) }
</div>
);
} ) }
{ error && (

View File

@ -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;

View File

@ -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 ) => {

View File

@ -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: (
<Fragment>
<Button isPrimary onClick={ this.createHomepage }>
<Button isPrimary isBusy={ isPending } onClick={ this.createHomepage }>
{ __( 'Create homepage', 'woocommerce-admin' ) }
</Button>
<Button

View File

@ -336,7 +336,7 @@ class ActivityPanel extends Component {
export default withSelect( ( select ) => {
const hasUnreadNotes = getUnreadNotes( select );
const hasUnreadOrders = getUnreadOrders( select );
const hasUnreadStock = getUnreadStock( select );
const hasUnreadStock = getUnreadStock();
const hasUnapprovedReviews = getUnapprovedReviews( select );
return {

View File

@ -112,21 +112,6 @@ export function getUnapprovedReviews( select ) {
return false;
}
export function getUnreadStock( select ) {
const { getItems, getItemsError, getItemsTotalCount } = select( 'wc-api' );
const productsQuery = {
page: 1,
per_page: 1,
low_in_stock: true,
status: 'publish',
};
getItems( 'products', productsQuery );
const isError = Boolean( getItemsError( 'products', productsQuery ) );
if ( isError ) {
return null;
}
const lowInStockCount = getItemsTotalCount( 'products', productsQuery );
return lowInStockCount > 0;
export function getUnreadStock() {
return getSetting( 'hasLowStock', false );
}

View File

@ -3,7 +3,6 @@
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import domReady from '@wordpress/dom-ready';
/**
@ -22,7 +21,11 @@ import { queueRecordEvent } from 'lib/tracks';
* @return {Promise} Promise for overlay existence.
*/
const saveStarted = () => {
if ( document.querySelector( '.editor-post-publish-button' ) === null ) {
if (
! document
.querySelector( '.editor-post-publish-button' )
.classList.contains( 'is-busy' )
) {
const promise = new Promise( ( resolve ) => {
window.requestAnimationFrame( resolve );
} );
@ -39,7 +42,9 @@ const saveStarted = () => {
*/
const saveCompleted = () => {
if (
document.querySelector( '.post-publish-panel__postpublish' ) === null
document
.querySelector( '.editor-post-publish-button' )
.classList.contains( 'is-busy' )
) {
const promise = new Promise( ( resolve ) => {
window.requestAnimationFrame( resolve );
@ -62,21 +67,11 @@ const onboardingHomepageNotice = () => {
saveButton.classList.add( 'is-clicked' );
saveCompleted().then( () => {
const postId = document.querySelector( '#post_ID' ).value;
const notificationType =
document.querySelector( '.components-snackbar__content' ) !== null
? 'snackbar'
: 'default';
apiFetch( {
path: '/wc-admin/options',
method: 'POST',
data: {
show_on_front: 'page',
page_on_front: postId,
},
} );
dispatch( 'core/notices' ).removeNotice( 'SAVE_POST_NOTICE_ID' );
dispatch( 'core/notices' ).createSuccessNotice(
__(
@ -107,21 +102,12 @@ const onboardingHomepageNotice = () => {
domReady( () => {
const publishButton = document.querySelector(
'.editor-post-publish-panel__toggle'
'.editor-post-publish-button'
);
if ( publishButton ) {
publishButton.addEventListener( 'click', function() {
saveStarted().then( () => {
const confirmButton = document.querySelector(
'.editor-post-publish-button'
);
if ( confirmButton ) {
confirmButton.addEventListener(
'click',
onboardingHomepageNotice
);
}
} );
} );
publishButton.addEventListener(
'click',
saveStarted().then( () => onboardingHomepageNotice() )
);
}
} );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/admin-library",
"version": "0.25.1",
"version": "0.26.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "@woocommerce/admin-library",
"version": "0.25.1",
"version": "0.26.1",
"homepage": "https://woocommerce.github.io/woocommerce-admin/",
"repository": {
"type": "git",

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { Button } from '@wordpress/components';
import { decodeEntities } from '@wordpress/html-entities';
import PropTypes from 'prop-types';
import classnames from 'classnames';
@ -26,7 +27,7 @@ const DropdownButton = ( props ) => {
>
<div className="woocommerce-dropdown-button__labels">
{ labels.map( ( label, i ) => (
<span key={ i }>{ label }</span>
<span key={ i }>{ decodeEntities( label ) }</span>
) ) }
</div>
</Button>

View File

@ -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
),
) ),
};
}

View File

@ -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;
}

View File

@ -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(
<Search type="products" allowFreeTextSearch />
);
const options = search
.instance()
.appendFreeTextSearch( [], 'Product Query' );
expect( options.length ).toBe( 1 );
} );
it( "doesn't show options with an empty search", () => {
const search = shallow(
<Search type="products" allowFreeTextSearch />
);
const options = search.instance().appendFreeTextSearch( [], '' );
expect( options.length ).toBe( 0 );
} );
it( 'returns an object with decoded text', () => {
const decodedText = computeSuggestionMatch(
'A test &amp; a &#116;&#101;&#115;&#116;',
'test'
);
const expected =
'{"suggestionBeforeMatch":"A ","suggestionMatch":"test","suggestionAfterMatch":" & a test"}';
expect( JSON.stringify( decodedText ) ).toBe( expected );
} );
} );

View File

@ -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',
{

View File

@ -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,
} );

View File

@ -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)

View File

@ -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(

View File

@ -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 '<!-- wp:cover {"url":"' . esc_url( $image['url'] ) . '","id":' . intval( $image['id'] ) . '} -->
<div class="wp-block-cover has-background-dim" style="background-image:url(' . esc_url( $image['url'] ) . ')"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
return '<!-- wp:cover {"url":"' . esc_url( $image['url'] ) . '","id":' . intval( $image['id'] ) . ',"dimRatio":0} -->
<div class="wp-block-cover" style="background-image:url(' . esc_url( $image['url'] ) . ')"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","textColor":"white","fontSize":"large"} -->
<p class="has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- wp:paragraph {"align":"center","textColor":"white"} -->
<p class="has-text-color has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:button {"align":"center"} -->
@ -202,13 +202,13 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
<!-- /wp:cover -->';
}
return '<!-- wp:cover {"overlayColor":"very-dark-gray"} -->
<div class="wp-block-cover has-very-dark-gray-background-color has-background-dim"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","fontSize":"large"} -->
<p class="has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce-admin' ) . '</p>
return '<!-- wp:cover {"dimRatio":0} -->
<div class="wp-block-cover"><div class="wp-block-cover__inner-container"><!-- wp:paragraph {"align":"center","placeholder":"' . __( 'Write title…', 'woocommerce-admin' ) . '","textColor":"white","fontSize":"large"} -->
<p class="has-text-color has-text-align-center has-large-font-size">' . __( 'Welcome to the store', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph {"align":"center"} -->
<p class="has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- wp:paragraph {"align":"center","textColor":"white"} -->
<p class="has-text-color has-text-align-center">' . __( 'Write a short welcome message here', 'woocommerce-admin' ) . '</p>
<!-- /wp:paragraph -->
<!-- wp:button {"align":"center"} -->
@ -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 ) ),
);

View File

@ -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;
}

View File

@ -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 );
}

View File

@ -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(),

View File

@ -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' );
}
/**

View File

@ -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;
}

View File

@ -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',
),
)
);
}

View File

@ -0,0 +1,82 @@
<?php
/**
* WooCommerce Admin: Update version reminder note.
*
* Creates a note to nudge users to use the newer version when two are installed.
*
* @package WooCommerce Admin
*/
namespace Automattic\WooCommerce\Admin\Notes;
defined( 'ABSPATH' ) || exit;
/**
* WC_Admin_Notes_Deactivate_Plugin.
*/
class WC_Admin_Notes_Deactivate_Plugin {
const NOTE_NAME = 'wc-admin-deactivate-plugin';
/**
* Attach hooks.
*/
public function __construct() {
add_action( 'init', array( $this, 'deactivate_feature_plugin' ) );
}
/**
* Creates the note to deactivate the older version.
*/
public static function add_note() {
$data_store = \WC_Data_Store::load( 'admin-note' );
$note_ids = $data_store->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;
}
}

View File

@ -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();
}
}

View File

@ -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
*