Merge pull request #22857 from woocommerce/feature/marketplace-suggestions

Feature/marketplace suggestions
This commit is contained in:
Mike Jolley 2019-03-07 16:39:14 +00:00 committed by GitHub
commit 5492369fe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1174 additions and 6 deletions

View File

@ -1,7 +1,7 @@
/* jshint node:true */
module.exports = function( grunt ) {
'use strict';
const sass = require( 'node-sass' );
var sass = require( 'node-sass' );
grunt.initConfig({

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4352,6 +4352,10 @@ img.help_tip {
content: "\f111";
}
&.marketplace-suggestions_options a::before {
content: none;
}
&.variations_options a::before {
content: "\f509";
}
@ -5950,6 +5954,27 @@ table.bar_chart {
}
}
.post-type-product .woocommerce-BlankState,
.post-type-shop_order .woocommerce-BlankState {
max-width: 764px;
text-align: center;
margin: auto;
.woocommerce-BlankState-message {
color: #444;
font-size: 1.5em;
margin: 0 auto 1em;
}
.woocommerce-BlankState-message::before {
font-size: 120px;
}
.woocommerce-BlankState-buttons {
margin-bottom: 4em;
}
}
/**
* Small screen optimisation
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,315 @@
/**
* marketplace-suggestions.scss
* Styling for in-product marketplace suggestions.
*/
@import "mixins";
@import "variables";
$suggestions-pale-gray: #ddd;
$suggestions-metabox-pale-gray: #eee;
$suggestions-copy-text: #444;
a.suggestion-dismiss {
border: none;
box-shadow: none;
color: $suggestions-pale-gray;
}
a.suggestion-dismiss:hover {
color: #aaa;
}
a.suggestion-dismiss::before {
@include iconbeforedashicons( "\f335" );
font-size: 1.5em;
}
#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab {
a span {
margin: 0;
}
}
.marketplace-suggestions-container.showing-suggestion {
text-align: left;
.marketplace-suggestion-container {
align-items: flex-start;
display: flex;
flex-direction: column;
// Allows us to position the dismiss x button
// relative to container on mobile.
position: relative;
img.marketplace-suggestion-icon {
height: 40px;
margin: 0;
margin-right: 1.5em;
flex: 0 0 40px;
}
.marketplace-suggestion-container-content {
flex: 1 1 60%;
h4 {
margin: 0;
}
p {
margin: 0;
margin-top: 4px;
color: $suggestions-copy-text;
}
}
.marketplace-suggestion-container-cta {
flex: 1 1 30%;
min-width: 160px;
text-align: right;
.suggestion-dismiss {
text-decoration: none;
position: absolute;
top: 1em;
right: 1em;
}
}
}
@media screen and (min-width: 600px) {
.marketplace-suggestion-container {
align-items: center;
flex-direction: row;
img.marketplace-suggestion-icon {
// display: inline-block;
}
}
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] {
.marketplace-suggestion-container {
.marketplace-suggestion-container-content {
h4 {
font-size: 1.1em;
margin: 0;
margin-bottom: 0;
}
}
}
}
// Additional breathing space margin under empty-state footer.
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] {
margin-bottom: 6em;
}
// Optimise footer suggestion layout for left-aligned CTA link button only.
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] {
.marketplace-suggestion-container {
flex-direction: row-reverse;
.marketplace-suggestion-container-cta {
text-align: left;
}
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] {
.marketplace-suggestion-container {
padding: 1em 1.5em;
.marketplace-suggestion-container-content {
p {
padding: 0;
line-height: 1.5;
}
}
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-inline"] {
// hide by default (mobile first)
display: none;
td:first-child {
border-left: 4px solid $woocommerce;
}
.marketplace-suggestion-container {
padding: 0.5em 0 0.5em 1em;
img.marketplace-suggestion-icon {
margin-bottom: 0;
}
.marketplace-suggestion-container-content {
h4 {
margin: 0;
font-size: 14px;
}
}
.marketplace-suggestion-container-cta {
.suggestion-dismiss {
position: relative;
top: 4px;
right: auto;
margin-left: 1em;
}
}
}
@media screen and (min-width: 800px) {
// Display inline table suggestion on desktop only.
// The table columns are dynamic, so there's no good way to style the row
// content correctly when columns are hidden.
display: table-row;
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] {
.marketplace-suggestion-container {
padding: 1.5em;
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"] {
.marketplace-suggestion-container {
padding: 0.75em 1.5em;
&:first-child {
padding-top: 1.5em;
}
&:last-child {
padding-bottom: 1.5em;
}
.marketplace-suggestion-container-content {
p:last-child {
margin-bottom: 0;
}
}
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] {
// hide by default (mobile first)
display: none;
.marketplace-suggestion-container .marketplace-suggestion-container-cta {
a.button {
display: inline-block;
min-width: 120px;
text-align: center;
margin: 0;
}
a.linkout {
font-size: 1.1em;
text-decoration: none;
}
a.linkout .dashicons {
margin-left: 4px;
bottom: 2px;
position: relative;
}
.suggestion-dismiss {
position: relative;
top: 5px;
right: auto;
margin-left: 1em;
}
}
@media screen and (min-width: 600px) {
// Display onboarding table suggestion on desktop only. (for now)
// There's limited room on mobile, and there are edge-case
// styling issues in some browsers.
display: block;
}
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"] {
border: none;
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] {
border: none;
border-top: 1px solid $suggestions-metabox-pale-gray;
border-bottom: 1px solid $suggestions-metabox-pale-gray;
}
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"],
.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"] {
border: 1px solid $suggestions-pale-gray;
border-bottom: none;
&:last-child {
border-bottom: 1px solid $suggestions-pale-gray;
}
}

View File

@ -0,0 +1,446 @@
/* global marketplace_suggestions, ajaxurl, Cookies */
( function( $, marketplace_suggestions, ajaxurl ) {
$( function() {
if ( 'undefined' === typeof marketplace_suggestions ) {
return;
}
// Stand-in wcTracks.recordEvent in case tracks is not available (for any reason).
window.wcTracks = window.wcTracks || {};
window.wcTracks.recordEvent = window.wcTracks.recordEvent || function() { };
// Tracks events sent in this file:
// - marketplace_suggestion_displayed
// - marketplace_suggestion_clicked
// - marketplace_suggestion_dismissed
// All are prefixed by {WC_Tracks::PREFIX}.
// All have one property for `suggestionSlug`, to identify the specific suggestion message.
// Dismiss the specified suggestion from the UI, and save the dismissal in settings.
function dismissSuggestion( context, suggestionSlug ) {
// hide the suggestion in the UI
var selector = '[data-suggestion-slug=' + suggestionSlug + ']';
$( selector ).fadeOut( function() {
$( this ).remove();
tidyProductEditMetabox();
} );
// save dismissal in user settings
jQuery.post(
ajaxurl,
{
'action': 'woocommerce_add_dismissed_marketplace_suggestion',
'_wpnonce': marketplace_suggestions.dismiss_suggestion_nonce,
'slug': suggestionSlug
}
);
// if this is a high-use area, delay new suggestion that area for a short while
var highUseSuggestionContexts = [ 'products-list-inline' ];
if ( _.contains( highUseSuggestionContexts, context ) ) {
Cookies.set( 'woocommerce_snooze_products_list_suggestions', '1', { expires: 2 } );
}
window.wcTracks.recordEvent( 'marketplace_suggestion_dismissed', {
suggestionSlug: suggestionSlug
} );
}
// Render DOM element for suggestion dismiss button.
function renderDismissButton( context, suggestionSlug ) {
var dismissButton = document.createElement( 'a' );
dismissButton.classList.add( 'suggestion-dismiss' );
dismissButton.setAttribute( 'title', marketplace_suggestions.i18n_marketplace_suggestions_dismiss_tooltip );
dismissButton.setAttribute( 'href', '#' );
dismissButton.onclick = function( event ) {
event.preventDefault();
dismissSuggestion( context, suggestionSlug );
};
return dismissButton;
}
function addUTMParameters( context, url ) {
var utmParams = {
utm_source: 'unknown',
utm_campaign: 'marketplacesuggestions',
utm_medium: 'product'
};
var sourceContextMap = {
'productstable': [
'products-list-inline'
],
'productsempty': [
'products-list-empty-header',
'products-list-empty-footer',
'products-list-empty-body'
],
'ordersempty': [
'orders-list-empty-header',
'orders-list-empty-footer',
'orders-list-empty-body'
],
'editproduct': [
'product-edit-meta-tab-header',
'product-edit-meta-tab-footer',
'product-edit-meta-tab-body'
]
};
var utmSource = _.findKey( sourceContextMap, function( sourceInfo ) {
return _.contains( sourceInfo, context );
} );
if ( utmSource ) {
utmParams.utm_source = utmSource;
}
return url + '?' + jQuery.param( utmParams );
}
// Render DOM element for suggestion linkout, optionally with button style.
function renderLinkout( context, slug, url, text, isButton ) {
var linkoutButton = document.createElement( 'a' );
var utmUrl = addUTMParameters( context, url );
linkoutButton.setAttribute( 'href', utmUrl );
linkoutButton.setAttribute( 'target', 'blank' );
linkoutButton.textContent = text;
linkoutButton.onclick = function() {
window.wcTracks.recordEvent( 'marketplace_suggestion_clicked', {
suggestionSlug: slug
} );
};
if ( isButton ) {
linkoutButton.classList.add( 'button' );
} else {
linkoutButton.classList.add( 'linkout' );
var linkoutIcon = document.createElement( 'span' );
linkoutIcon.classList.add( 'dashicons', 'dashicons-external' );
linkoutButton.appendChild( linkoutIcon );
}
return linkoutButton;
}
// Render DOM element for suggestion icon image.
function renderSuggestionIcon( iconUrl ) {
if ( ! iconUrl ) {
return null;
}
var image = document.createElement( 'img' );
image.src = iconUrl;
image.classList.add( 'marketplace-suggestion-icon' );
return image;
}
// Render DOM elements for suggestion content.
function renderSuggestionContent( title, copy ) {
var container = document.createElement( 'div' );
container.classList.add( 'marketplace-suggestion-container-content' );
if ( title ) {
var titleHeading = document.createElement( 'h4' );
titleHeading.textContent = title;
container.appendChild( titleHeading );
}
if ( copy ) {
var body = document.createElement( 'p' );
body.textContent = copy;
container.appendChild( body );
}
return container;
}
// Render DOM elements for suggestion call-to-action button or link with dismiss 'x'.
function renderSuggestionCTA( context, slug, url, linkText, linkIsButton, allowDismiss ) {
var container = document.createElement( 'div' );
if ( ! linkText ) {
linkText = marketplace_suggestions.i18n_marketplace_suggestions_default_cta;
}
container.classList.add( 'marketplace-suggestion-container-cta' );
if ( url && linkText ) {
var linkoutElement = renderLinkout( context, slug, url, linkText, linkIsButton );
container.appendChild( linkoutElement );
}
if ( allowDismiss ) {
container.appendChild( renderDismissButton( context, slug ) );
}
return container;
}
function getTableBannerColspan() {
return $( 'table.wp-list-table.posts thead th:not(.hidden)' ).length + 1;
}
// Render a "table banner" style suggestion.
// These are used in admin lists, e.g. products list.
function renderTableBanner( context, slug, iconUrl, title, copy, url, buttonText, allowDismiss ) {
if ( ! title || ! url ) {
return;
}
var row = document.createElement( 'tr' );
row.classList.add( 'marketplace-table-banner' );
row.classList.add( 'marketplace-suggestions-container' );
row.classList.add( 'showing-suggestion' );
row.dataset.marketplaceSuggestionsContext = 'products-list-inline';
row.dataset.suggestionSlug = slug;
var cell = document.createElement( 'td' );
cell.classList.add( 'marketplace-table-banner-td' );
cell.setAttribute( 'colspan', getTableBannerColspan() );
var container = document.createElement( 'div' );
container.classList.add( 'marketplace-suggestion-container' );
container.dataset.suggestionSlug = slug;
var icon = renderSuggestionIcon( iconUrl );
if ( icon ) {
container.appendChild( icon );
}
container.appendChild(
renderSuggestionContent( title, copy )
);
container.appendChild(
renderSuggestionCTA( context, slug, url, buttonText, true, allowDismiss )
);
cell.appendChild( container );
row.appendChild( cell );
return row;
}
// Render a "list item" style suggestion.
// These are used in onboarding style contexts, e.g. products list empty state.
function renderListItem( context, slug, iconUrl, title, copy, url, linkText, linkIsButton, allowDismiss ) {
var container = document.createElement( 'div' );
container.classList.add( 'marketplace-suggestion-container' );
container.dataset.suggestionSlug = slug;
var icon = renderSuggestionIcon( iconUrl );
if ( icon ) {
container.appendChild( icon );
}
container.appendChild(
renderSuggestionContent( title, copy )
);
container.appendChild(
renderSuggestionCTA( context, slug, url, linkText, linkIsButton, allowDismiss )
);
return container;
}
// Filter suggestion data to remove less-relevant suggestions.
function getRelevantPromotions( marketplaceSuggestionsApiData, displayContext ) {
// select based on display context
var promos = _.filter( marketplaceSuggestionsApiData, function( promo ) {
if ( _.isArray( promo.context ) ) {
return _.contains( promo.context, displayContext );
}
return ( displayContext === promo.context );
} );
// hide promos the user has dismissed
promos = _.filter( promos, function( promo ) {
return ! _.contains( marketplace_suggestions.dismissed_suggestions, promo.slug );
} );
// hide promos for things the user already has installed
promos = _.filter( promos, function( promo ) {
return ! _.contains( marketplace_suggestions.active_plugins, promo['hide-if-active'] );
} );
// hide promos that are not applicable based on user's installed extensions
promos = _.filter( promos, function( promo ) {
if ( ! promo['show-if-active'] ) {
// this promotion is relevant to all
return true;
}
// if the user has any of the prerequisites, show the promo
return ( _.intersection( marketplace_suggestions.active_plugins, promo['show-if-active'] ).length > 0 );
} );
return promos;
}
// Show and hide page elements dependent on suggestion state.
function hidePageElementsForSuggestionState( usedSuggestionsContexts ) {
var showingEmptyStateSuggestions = _.intersection(
usedSuggestionsContexts,
[ 'products-list-empty-body', 'orders-list-empty-body' ]
).length > 0;
// Streamline onboarding UI if we're in 'empty state' welcome mode.
if ( showingEmptyStateSuggestions ) {
$( '#screen-meta-links' ).hide();
$( '#wpfooter' ).hide();
}
// Hide the header & footer, they don't make sense without specific promotion content
if ( ! showingEmptyStateSuggestions ) {
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-header"]' ).hide();
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="products-list-empty-footer"]' ).hide();
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-header"]' ).hide();
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="orders-list-empty-footer"]' ).hide();
}
}
// Streamline the product edit suggestions tab dependent on what's visible.
function tidyProductEditMetabox() {
var productMetaboxSuggestions = $(
'.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]'
).children();
if ( 0 >= productMetaboxSuggestions.length ) {
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-header"]' ).slideUp();
$( '.marketplace-suggestions-container[data-marketplace-suggestions-context="product-edit-meta-tab-body"]' ).fadeOut();
}
}
function refreshBannerColspanForScreenOptions( content ) {
$( '#show-settings-link' ).on( 'focus.scroll-into-view', function() {
$( '.marketplace-table-banner-td' ).attr( 'colspan', getTableBannerColspan() );
});
}
// Render suggestion data in appropriate places in UI.
function displaySuggestions( marketplaceSuggestionsApiData ) {
var usedSuggestionsContexts = [];
// iterate over all suggestions containers, rendering promos
$( '.marketplace-suggestions-container' ).each( function() {
// determine the context / placement we're populating
var context = this.dataset.marketplaceSuggestionsContext;
// find promotions that target this context
var promos = getRelevantPromotions( marketplaceSuggestionsApiData, context );
// shuffle/randomly select five suggestions to display
var suggestionsToDisplay = _.sample( promos, 5 );
// render the promo content
for ( var i in suggestionsToDisplay ) {
var linkText = suggestionsToDisplay[ i ]['link-text'];
var linkoutIsButton = true;
if ( suggestionsToDisplay[ i ]['link-text'] ) {
linkText = suggestionsToDisplay[ i ]['link-text'];
linkoutIsButton = false;
}
// dismiss is allowed by default
var allowDismiss = true;
if ( suggestionsToDisplay[ i ]['allow-dismiss'] === false ) {
allowDismiss = false;
}
var content = renderListItem(
context,
suggestionsToDisplay[ i ].slug,
suggestionsToDisplay[ i ].icon,
suggestionsToDisplay[ i ].title,
suggestionsToDisplay[ i ].copy,
suggestionsToDisplay[ i ].url,
linkText,
linkoutIsButton,
allowDismiss
);
$( this ).append( content );
$( this ).addClass( 'showing-suggestion' );
usedSuggestionsContexts.push( context );
window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', {
suggestionSlug: suggestionsToDisplay[ i ].slug
} );
}
} );
// render inline promos in products list
if ( 0 === usedSuggestionsContexts.length ) {
$( '.wp-admin.admin-bar.edit-php.post-type-product table.wp-list-table.posts tbody').first().each( function() {
var context = 'products-list-inline';
// product list banner suggestion is temporarily suppressed after a recent dismissal
if ( Cookies.get( 'woocommerce_snooze_products_list_suggestions' ) ) {
return;
}
// find promotions that target this context
var promos = getRelevantPromotions( marketplaceSuggestionsApiData, context );
if ( ! promos || ! promos.length ) {
return;
}
// shuffle/randomly select the suggestion to display
var suggestionToDisplay = _.sample( promos );
// dismiss is allowed by default
var allowDismiss = true;
if ( false === suggestionToDisplay['allow-dismiss'] ) {
allowDismiss = false;
}
// render first promo
var content = renderTableBanner(
context,
suggestionToDisplay.slug,
suggestionToDisplay.icon,
suggestionToDisplay.title,
suggestionToDisplay.copy,
suggestionToDisplay.url,
suggestionToDisplay['button-text'],
allowDismiss
);
if ( content ) {
// where should we put it in the list?
var rows = $( this ).children();
var minRow = 3;
$( content ).hide();
if ( rows.length <= minRow ) {
// if small number of rows, append at end
$( this ).append( content );
}
else {
// for more rows, insert
$( rows[ minRow - 1 ] ).after( content );
}
$( content ).fadeIn();
usedSuggestionsContexts.push( context );
refreshBannerColspanForScreenOptions( content );
window.wcTracks.recordEvent( 'marketplace_suggestion_displayed', {
suggestionSlug: suggestionToDisplay.slug
} );
}
} );
}
hidePageElementsForSuggestionState( usedSuggestionsContexts );
tidyProductEditMetabox();
}
if ( marketplace_suggestions.suggestions_data ) {
displaySuggestions( marketplace_suggestions.suggestions_data );
}
});
})( jQuery, marketplace_suggestions, ajaxurl );

File diff suppressed because one or more lines are too long

View File

@ -343,4 +343,5 @@
}
});
});
})( jQuery, woocommerce_admin );

View File

@ -40,12 +40,14 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), WC_VERSION );
wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), WC_VERSION );
wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), WC_VERSION, 'print' );
wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), WC_VERSION );
// Add RTL support for admin styles.
wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' );
wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' );
wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' );
wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' );
wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' );
// Sitewide menu CSS.
wp_enqueue_style( 'woocommerce_admin_menu_styles' );
@ -70,6 +72,10 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
do_action( 'woocommerce_admin_css' );
wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' );
}
if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) {
wp_enqueue_style( 'woocommerce_admin_marketplace_styles' );
}
}
@ -108,6 +114,8 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' );
wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.4' );
wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), WC_VERSION );
wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true );
wp_localize_script(
'wc-enhanced-select',
'wc_enhanced_select_params',
@ -416,7 +424,35 @@ if ( ! class_exists( 'WC_Admin_Assets', false ) ) :
)
);
}
if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) {
$active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) );
wp_register_script(
'marketplace-suggestions',
WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js',
array( 'jquery', 'underscore', 'js-cookie' ),
WC_VERSION,
true
);
wp_localize_script(
'marketplace-suggestions',
'marketplace_suggestions',
array(
'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ),
'active_plugins' => $active_plugin_slugs,
'dismissed_suggestions' => WC_Marketplace_Suggestions::get_dismissed_suggestions(),
'suggestions_data' => WC_Marketplace_Suggestions::get_suggestions_api_data(),
'i18n_marketplace_suggestions_default_cta'
=> esc_html__( 'Learn More', 'woocommerce' ),
'i18n_marketplace_suggestions_dismiss_tooltip'
=> esc_attr__( 'Dismiss this suggestion', 'woocommerce' ),
)
);
wp_enqueue_script( 'marketplace-suggestions' );
}
}
}
endif;

View File

@ -85,6 +85,10 @@ class WC_Admin {
include_once dirname( __FILE__ ) . '/helper/class-wc-helper-plugin-info.php';
include_once dirname( __FILE__ ) . '/helper/class-wc-helper-compat.php';
include_once dirname( __FILE__ ) . '/helper/class-wc-helper.php';
// Marketplace suggestions & related REST API.
include_once dirname( __FILE__ ) . '/marketplace-suggestions/class-wc-marketplace-suggestions.php';
include_once dirname( __FILE__ ) . '/marketplace-suggestions/class-wc-marketplace-updater.php';
}
/**

View File

@ -1007,7 +1007,7 @@ class WC_Helper {
}
/**
* Obtain a list of locally installed Woo extensions.
* Obtain a list of data about locally installed Woo extensions.
*/
public static function get_local_woo_plugins() {
if ( ! function_exists( 'get_plugins' ) ) {
@ -1046,6 +1046,7 @@ class WC_Helper {
$data['_product_id'] = absint( $product_id );
$data['_file_id'] = $file_id;
$data['_type'] = 'plugin';
$data['slug'] = dirname( $filename );
$woo_plugins[ $filename ] = $data;
}

View File

@ -47,9 +47,16 @@ class WC_Admin_List_Table_Orders extends WC_Admin_List_Table {
*/
protected function render_blank_state() {
echo '<div class="woocommerce-BlankState">';
echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'When you receive a new order, it will appear here.', 'woocommerce' ) . '</h2>';
echo '<div class="woocommerce-BlankState-buttons">';
echo '<a class="woocommerce-BlankState-cta button-primary button" target="_blank" href="https://docs.woocommerce.com/document/managing-orders/?utm_source=blankslate&utm_medium=product&utm_content=ordersdoc&utm_campaign=woocommerceplugin">' . esc_html__( 'Learn more about orders', 'woocommerce' ) . '</a>';
echo '</div>';
do_action( 'wc_marketplace_suggestions_orders_empty_state' );
echo '</div>';
}
/**

View File

@ -47,9 +47,18 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
*/
protected function render_blank_state() {
echo '<div class="woocommerce-BlankState">';
echo '<h2 class="woocommerce-BlankState-message">' . esc_html__( 'Ready to start selling something awesome?', 'woocommerce' ) . '</h2>';
echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ) . '">' . esc_html__( 'Create your first product!', 'woocommerce' ) . '</a>';
echo '<a class="woocommerce-BlankState-cta button" href="' . esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) . '">' . esc_html__( 'Import products from a CSV file', 'woocommerce' ) . '</a>';
echo '<div class="woocommerce-BlankState-buttons">';
echo '<a class="woocommerce-BlankState-cta button-primary button" href="' . esc_url( admin_url( 'post-new.php?post_type=product&tutorial=true' ) ) . '">' . esc_html__( 'Create Product', 'woocommerce' ) . '</a>';
echo '<a class="woocommerce-BlankState-cta button" href="' . esc_url( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) . '">' . esc_html__( 'Start Import', 'woocommerce' ) . '</a>';
echo '</div>';
do_action( 'wc_marketplace_suggestions_products_empty_state' );
echo '</div>';
}

View File

@ -0,0 +1,197 @@
<?php
/**
* Marketplace suggestions
*
* Behaviour for displaying in-context suggestions for marketplace extensions.
*
* @package WooCommerce\Classes
* @since 3.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Marketplace suggestions core behaviour.
*/
class WC_Marketplace_Suggestions {
/**
* Initialise.
*/
public static function init() {
if ( ! self::allow_suggestions() ) {
return;
}
// Add suggestions to the product tabs.
add_action( 'woocommerce_product_data_tabs', array( __CLASS__, 'product_data_tabs' ) );
add_action( 'woocommerce_product_data_panels', array( __CLASS__, 'product_data_panels' ) );
// Register ajax api handlers.
add_action( 'wp_ajax_woocommerce_add_dismissed_marketplace_suggestion', array( __CLASS__, 'post_add_dismissed_suggestion_handler' ) );
// Register hooks for rendering suggestions container markup.
add_action( 'wc_marketplace_suggestions_products_empty_state', array( __CLASS__, 'render_products_list_empty_state' ) );
add_action( 'wc_marketplace_suggestions_orders_empty_state', array( __CLASS__, 'render_orders_list_empty_state' ) );
}
/**
* Product data tabs filter
*
* Adds a new Extensions tab to the product data meta box.
*
* @param array $tabs Existing tabs.
*
* @return array
*/
public static function product_data_tabs( $tabs ) {
$tabs['marketplace-suggestions'] = array(
'label' => _x( 'Get more options', 'Marketplace suggestions', 'woocommerce' ),
'target' => 'marketplace_suggestions',
'class' => array(),
'priority' => 1000,
);
return $tabs;
}
/**
* Render additional panels in the proudct data metabox.
*/
public static function product_data_panels() {
include dirname( __FILE__ ) . '/templates/html-product-data-extensions.php';
}
/**
* Return an array of suggestions the user has dismissed.
*/
public static function get_dismissed_suggestions() {
$dismissed_suggestions = array();
$dismissed_suggestions_data = get_user_meta( get_current_user_id(), 'wc_marketplace_suggestions_dismissed_suggestions', true );
if ( $dismissed_suggestions_data ) {
$dismissed_suggestions = $dismissed_suggestions_data;
if ( ! is_array( $dismissed_suggestions ) ) {
$dismissed_suggestions = array();
}
}
return $dismissed_suggestions;
}
/**
* POST handler for adding a dismissed suggestion.
*/
public static function post_add_dismissed_suggestion_handler() {
if ( ! check_ajax_referer( 'add_dismissed_marketplace_suggestion' ) ) {
wp_die();
}
$post_data = wp_unslash( $_POST );
$suggestion_slug = sanitize_text_field( $post_data['slug'] );
if ( ! $suggestion_slug ) {
wp_die();
}
$dismissed_suggestions = self::get_dismissed_suggestions();
if ( in_array( $suggestion_slug, $dismissed_suggestions, true ) ) {
wp_die();
}
$dismissed_suggestions[] = $suggestion_slug;
update_user_meta(
get_current_user_id(),
'wc_marketplace_suggestions_dismissed_suggestions',
$dismissed_suggestions
);
wp_die();
}
/**
* Render suggestions containers in products list empty state.
*/
public static function render_products_list_empty_state() {
self::render_suggestions_container( 'products-list-empty-header' );
self::render_suggestions_container( 'products-list-empty-body' );
self::render_suggestions_container( 'products-list-empty-footer' );
}
/**
* Render suggestions containers in orders list empty state.
*/
public static function render_orders_list_empty_state() {
self::render_suggestions_container( 'orders-list-empty-header' );
self::render_suggestions_container( 'orders-list-empty-body' );
self::render_suggestions_container( 'orders-list-empty-footer' );
}
/**
* Render a suggestions container element, with the specified context.
*
* @param string $context Suggestion context name (rendered as a css class).
*/
public static function render_suggestions_container( $context ) {
include dirname( __FILE__ ) . '/views/container.php';
}
/**
* Should suggestions be displayed?
*
* @param string $screen_id The current admin screen.
*
* @return bool
*/
public static function show_suggestions_for_screen( $screen_id ) {
// We only show suggestions on certain admin screens.
if ( ! in_array( $screen_id, array( 'edit-product', 'edit-shop_order', 'product' ), true ) ) {
return false;
}
return self::allow_suggestions();
}
/**
* Should suggestions be displayed?
*
* @return bool
*/
public static function allow_suggestions() {
// We currently only support English suggestions.
$locale = get_locale();
$suggestion_locales = array(
'en_AU',
'en_CA',
'en_GB',
'en_NZ',
'en_US',
'en_ZA',
);
if ( ! in_array( $locale, $suggestion_locales, true ) ) {
return false;
}
// Suggestions are only displayed if user can install plugins.
if ( ! current_user_can( 'install_plugins' ) ) {
return false;
}
// User can disabled all suggestions via filter.
return apply_filters( 'woocommerce_allow_marketplace_suggestions', true );
}
/**
* Pull suggestion data from remote endpoint & cache in a transient.
*
* @return array of json API data
*/
public static function get_suggestions_api_data() {
$data = get_option( 'woocommerce_marketplace_suggestions', array() );
return ! empty( $data['suggestions'] ) ? $data['suggestions'] : array();
}
}
WC_Marketplace_Suggestions::init();

View File

@ -0,0 +1,86 @@
<?php
/**
* Marketplace suggestions updater
*
* Uses WC_Queue to ensure marketplace suggestions data is up to date and cached locally.
*
* @package WooCommerce\Classes
* @since 3.6.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Marketplace Suggestions Updater
*/
class WC_Marketplace_Updater {
/**
* Setup.
*/
public static function load() {
add_action( 'init', array( __CLASS__, 'init' ) );
}
/**
* Schedule events and hook appropriate actions.
*/
public static function init() {
$queue = WC()->queue();
$next = $queue->get_next( 'woocommerce_update_marketplace_suggestions' );
if ( ! $next ) {
$queue->schedule_recurring( time(), WEEK_IN_SECONDS, 'woocommerce_update_marketplace_suggestions' );
}
add_action( 'woocommerce_update_marketplace_suggestions', array( __CLASS__, 'update_marketplace_suggestions' ) );
}
/**
* Fetches new marketplace data, updates wc_marketplace_suggestions.
*/
public static function update_marketplace_suggestions() {
$data = get_option(
'woocommerce_marketplace_suggestions',
array(
'suggestions' => array(),
'updated' => time(),
)
);
$data['updated'] = time();
$url = 'https://woocommerce.com/wp-json/wccom/marketplace-suggestions/1.0/suggestions.json';
$request = wp_safe_remote_get( $url );
if ( is_wp_error( $request ) ) {
self::retry();
return update_option( 'woocommerce_marketplace_suggestions', $data, false );
}
$body = wp_remote_retrieve_body( $request );
if ( empty( $body ) ) {
self::retry();
return update_option( 'woocommerce_marketplace_suggestions', $data, false );
}
$body = json_decode( $body, true );
if ( empty( $body ) || ! is_array( $body ) ) {
self::retry();
return update_option( 'woocommerce_marketplace_suggestions', $data, false );
}
$data['suggestions'] = $body;
return update_option( 'woocommerce_marketplace_suggestions', $data, false );
}
/**
* Used when an error has occured when fetching suggestions.
* Re-schedules the job earlier than the main weekly one.
*/
public static function retry() {
WC()->queue()->cancel( 'woocommerce_update_marketplace_suggestions' );
WC()->queue()->schedule_single( time() + DAY_IN_SECONDS, 'woocommerce_update_marketplace_suggestions' );
}
}
WC_Marketplace_Updater::load();

View File

@ -0,0 +1,20 @@
<?php
/**
* The marketplace suggestions tab HTML in the product tabs
*
* @package WooCommerce\Classes
* @since 3.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div id="marketplace_suggestions" class="panel woocommerce_options_panel hidden">
<?php
WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-header' );
WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-body' );
WC_Marketplace_Suggestions::render_suggestions_container( 'product-edit-meta-tab-footer' );
?>
</div>

View File

@ -0,0 +1,17 @@
<?php
/**
* Marketplace suggestions container
*
* @package WooCommerce/Templates
* @version 3.6.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="marketplace-suggestions-container"
data-marketplace-suggestions-context="<?php echo esc_attr( $context ); ?>"
>
</div>

View File

@ -372,6 +372,7 @@ final class WooCommerce {
include_once WC_ABSPATH . 'includes/class-wc-logger.php';
include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php';
include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php';
include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php';
/**
* Data stores - used to store and retrieve CRUD object data from the database.