Working attributes select UI

This commit is contained in:
claudiulodro 2018-02-20 11:47:50 -08:00
parent af5749397a
commit 9148e9801b
6 changed files with 691 additions and 15 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -166,12 +166,14 @@
} }
} }
.product-category-select { .product-category-select,
.product-attribute-select {
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
#product-category-search { #product-category-search,
#product-attribute-search {
width: 100%; width: 100%;
margin: 0 0 10px; margin: 0 0 10px;
padding: 10px 15px; padding: 10px 15px;
@ -179,7 +181,8 @@
border-color: #E6EAEE; border-color: #E6EAEE;
} }
.product-categories-list { .product-categories-list,
.product-attributes-list {
height: 200px; height: 200px;
overflow-y: scroll; overflow-y: scroll;
@ -255,7 +258,8 @@
text-decoration: none; text-decoration: none;
} }
.product-category-count { .product-category-count,
.product-attribute-count {
padding: 0 10px; padding: 0 10px;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
border-radius: 15px; border-radius: 15px;
@ -263,6 +267,24 @@
font-size: .8em; font-size: .8em;
margin-left: auto; margin-left: auto;
} }
.product-attribute {
border: 1px solid #e9e9e9;
.product-attribute-name {
padding: .5em;
background: #FFFFFF;
border-bottom: 1px solid #e9e9e9;
.product-attribute-count {
float: right;
}
}
}
}
input[type="radio"] {
border-radius: 10px;
} }
&:after { &:after {

View File

@ -327,7 +327,7 @@ var ProductsBlockSettingsEditor = function (_React$Component3) {
} else if ('category' === this.state.display) { } else if ('category' === this.state.display) {
extra_settings = wp.element.createElement(_categorySelect.ProductsCategorySelect, this.props); extra_settings = wp.element.createElement(_categorySelect.ProductsCategorySelect, this.props);
} else if ('attribute' === this.state.display) { } else if ('attribute' === this.state.display) {
extra_settings = wp.element.createElement(_attributeSelect.ProductsAttributeSelect, null); extra_settings = wp.element.createElement(_attributeSelect.ProductsAttributeSelect, this.props);
} }
var menu = this.state.menu_visible ? wp.element.createElement(ProductsBlockSettingsEditorDisplayOptions, { existing: this.state.display ? true : false, update_display_callback: this.updateDisplay }) : null; var menu = this.state.menu_visible ? wp.element.createElement(ProductsBlockSettingsEditorDisplayOptions, { existing: this.state.display ? true : false, update_display_callback: this.updateDisplay }) : null;
@ -1493,6 +1493,12 @@ var _wp$components = wp.components,
withAPIData = _wp$components.withAPIData, withAPIData = _wp$components.withAPIData,
Dropdown = _wp$components.Dropdown; Dropdown = _wp$components.Dropdown;
/**
* Attribute data cache. Needed because it takes a lot of API calls to generate attribute info.
*/
var PRODUCT_ATTRIBUTE_DATA = {};
/** /**
* When the display mode is 'Attribute' search for and select product attributes to pull products from. * When the display mode is 'Attribute' search for and select product attributes to pull products from.
*/ */
@ -1500,19 +1506,123 @@ var _wp$components = wp.components,
var ProductsAttributeSelect = exports.ProductsAttributeSelect = function (_React$Component) { var ProductsAttributeSelect = exports.ProductsAttributeSelect = function (_React$Component) {
_inherits(ProductsAttributeSelect, _React$Component); _inherits(ProductsAttributeSelect, _React$Component);
function ProductsAttributeSelect() { /**
* Constructor.
*/
function ProductsAttributeSelect(props) {
_classCallCheck(this, ProductsAttributeSelect); _classCallCheck(this, ProductsAttributeSelect);
return _possibleConstructorReturn(this, (ProductsAttributeSelect.__proto__ || Object.getPrototypeOf(ProductsAttributeSelect)).apply(this, arguments)); var _this = _possibleConstructorReturn(this, (ProductsAttributeSelect.__proto__ || Object.getPrototypeOf(ProductsAttributeSelect)).call(this, props));
_this.state = {
selectedAttribute: '',
selectedTerms: [],
filterQuery: ''
};
_this.setSelectedAttribute = _this.setSelectedAttribute.bind(_this);
_this.addTerm = _this.addTerm.bind(_this);
_this.removeTerm = _this.removeTerm.bind(_this);
return _this;
} }
/**
* Set the selected attribute.
*
* @param slug string Attribute slug.
*/
_createClass(ProductsAttributeSelect, [{ _createClass(ProductsAttributeSelect, [{
key: "render", key: 'setSelectedAttribute',
value: function setSelectedAttribute(slug) {
this.setState({
selectedAttribute: slug,
selectedTerms: []
});
}
/**
* Add a term to the selected attribute's terms.
*
* @param slug string Term slug.
*/
}, {
key: 'addTerm',
value: function addTerm(slug) {
var terms = this.state.selectedTerms;
terms.push(slug);
this.setState({
selectedTerms: terms
});
}
/**
* Remove a term from the selected attribute's terms.
*
* @param slug string Term slug.
*/
}, {
key: 'removeTerm',
value: function removeTerm(slug) {
var newTerms = [];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = this.state.selectedTerms[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var termSlug = _step.value;
if (termSlug !== slug) {
newTerms.push(termSlug);
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
this.setState({
selectedTerms: newTerms
});
}
/**
* Render the whole section.
*/
}, {
key: 'render',
value: function render() { value: function render() {
// @todo Remove this once data is moving around properly.
console.log("STATE UPDATED");
console.log(this.state);
return wp.element.createElement( return wp.element.createElement(
"div", 'div',
{ className: "product-attribute-select" }, { className: 'product-attribute-select' },
"TODO: Attribute select screen" wp.element.createElement(ProductAttributeFilter, null),
wp.element.createElement(ProductAttributeList, {
selectedAttribute: this.state.selectedAttribute,
selectedTerms: this.state.selectedTerms,
setSelectedAttribute: this.setSelectedAttribute.bind(this),
addTerm: this.addTerm.bind(this),
removeTerm: this.removeTerm.bind(this)
})
); );
} }
}]); }]);
@ -1520,5 +1630,284 @@ var ProductsAttributeSelect = exports.ProductsAttributeSelect = function (_React
return ProductsAttributeSelect; return ProductsAttributeSelect;
}(React.Component); }(React.Component);
/**
* Search area for filtering through the attributes list.
*/
var ProductAttributeFilter = function ProductAttributeFilter() {
return wp.element.createElement(
'div',
null,
wp.element.createElement('input', { id: 'product-attribute-search', type: 'search', placeholder: __('Search for attributes') })
);
};
/**
* List of attributes.
*/
var ProductAttributeList = withAPIData(function (props) {
return {
attributes: '/wc/v2/products/attributes'
};
})(function (_ref) {
var attributes = _ref.attributes,
selectedAttribute = _ref.selectedAttribute,
selectedTerms = _ref.selectedTerms,
setSelectedAttribute = _ref.setSelectedAttribute,
addTerm = _ref.addTerm,
removeTerm = _ref.removeTerm;
if (!attributes.data) {
return __('Loading');
}
if (0 === attributes.data.length) {
return __('No attributes found');
}
var attributeElements = [];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = attributes.data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var attribute = _step2.value;
if (PRODUCT_ATTRIBUTE_DATA.hasOwnProperty(attribute.slug)) {
attributeElements.push(wp.element.createElement(ProductAttributeElement, {
selectedAttribute: selectedAttribute,
selectedTerms: selectedTerms,
attribute: attribute,
setSelectedAttribute: setSelectedAttribute,
addTerm: addTerm,
removeTerm: removeTerm
}));
} else {
attributeElements.push(wp.element.createElement(UncachedProductAttributeElement, {
selectedAttribute: selectedAttribute,
selectedTerms: selectedTerms,
attribute: attribute,
setSelectedAttribute: setSelectedAttribute,
addTerm: addTerm,
removeTerm: removeTerm
}));
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return wp.element.createElement(
'div',
{ className: 'product-attributes-list' },
attributeElements
);
});
/**
* Caches then renders a product attribute term element.
*/
var UncachedProductAttributeElement = withAPIData(function (props) {
return {
terms: '/wc/v2/products/attributes/' + props.attribute.id + '/terms'
};
})(function (_ref2) {
var terms = _ref2.terms,
selectedAttribute = _ref2.selectedAttribute,
selectedTerms = _ref2.selectedTerms,
attribute = _ref2.attribute,
setSelectedAttribute = _ref2.setSelectedAttribute,
addTerm = _ref2.addTerm,
removeTerm = _ref2.removeTerm;
if (!terms.data) {
return __('Loading');
}
if (0 === terms.data.length) {
return __('No attribute options found');
}
// Populate cache.
PRODUCT_ATTRIBUTE_DATA[attribute.slug] = { terms: [] };
var totalCount = 0;
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = terms.data[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var term = _step3.value;
totalCount += term.count;
PRODUCT_ATTRIBUTE_DATA[attribute.slug].terms.push(term);
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
PRODUCT_ATTRIBUTE_DATA[attribute.slug].count = totalCount;
return wp.element.createElement(ProductAttributeElement, {
selectedAttribute: selectedAttribute,
selectedTerms: selectedTerms,
attribute: attribute,
setSelectedAttribute: setSelectedAttribute,
addTerm: addTerm,
removeTerm: removeTerm
});
});
/**
* A product attribute term element.
*/
var ProductAttributeElement = function (_React$Component2) {
_inherits(ProductAttributeElement, _React$Component2);
/**
* Constructor.
*/
function ProductAttributeElement(props) {
_classCallCheck(this, ProductAttributeElement);
var _this2 = _possibleConstructorReturn(this, (ProductAttributeElement.__proto__ || Object.getPrototypeOf(ProductAttributeElement)).call(this, props));
_this2.handleAttributeChange = _this2.handleAttributeChange.bind(_this2);
_this2.handleTermChange = _this2.handleTermChange.bind(_this2);
return _this2;
}
/**
* Propagate and reset values when the selected attribute is changed.
*
* @param evt Event object
*/
_createClass(ProductAttributeElement, [{
key: 'handleAttributeChange',
value: function handleAttributeChange(evt) {
var slug = evt.target.value;
if (this.props.selectedAttribute === slug) {
return;
}
this.props.setSelectedAttribute(slug);
}
/**
* Add or remove selected terms.
*
* @param evt Event object
*/
}, {
key: 'handleTermChange',
value: function handleTermChange(evt) {
if (evt.target.checked) {
this.props.addTerm(evt.target.value);
} else {
this.props.removeTerm(evt.target.value);
}
}
/**
* Render the details for one attribute.
*/
}, {
key: 'render',
value: function render() {
var _this3 = this;
var attribute = PRODUCT_ATTRIBUTE_DATA[this.props.attribute.slug];
var isSelected = this.props.selectedAttribute === this.props.attribute.slug;
var attributeTerms = null;
if (isSelected) {
attributeTerms = wp.element.createElement(
'ul',
{ className: 'product-attribute-terms' },
attribute.terms.map(function (term) {
return wp.element.createElement(
'li',
{ className: 'product-attribute-term' },
wp.element.createElement(
'label',
null,
wp.element.createElement('input', { type: 'checkbox',
value: term.slug,
onChange: _this3.handleTermChange,
checked: _this3.props.selectedTerms.includes(term.slug)
}),
term.name,
wp.element.createElement(
'span',
{ className: 'product-attribute-count' },
term.count
)
)
);
})
);
}
return wp.element.createElement(
'div',
{ className: 'product-attribute' },
wp.element.createElement(
'div',
{ className: 'product-attribute-name' },
wp.element.createElement(
'label',
null,
wp.element.createElement('input', { type: 'radio',
value: this.props.attribute.slug,
onClick: this.handleAttributeChange,
checked: isSelected
}),
this.props.attribute.name,
wp.element.createElement(
'span',
{ className: 'product-attribute-count' },
attribute.count
)
)
),
attributeTerms
);
}
}]);
return ProductAttributeElement;
}(React.Component);
/***/ }) /***/ })
/******/ ]); /******/ ]);

View File

@ -173,7 +173,7 @@ class ProductsBlockSettingsEditor extends React.Component {
} else if ( 'category' === this.state.display ) { } else if ( 'category' === this.state.display ) {
extra_settings = <ProductsCategorySelect { ...this.props } />; extra_settings = <ProductsCategorySelect { ...this.props } />;
} else if ( 'attribute' === this.state.display ) { } else if ( 'attribute' === this.state.display ) {
extra_settings = <ProductsAttributeSelect /> extra_settings = <ProductsAttributeSelect { ...this.props } />
} }
const menu = this.state.menu_visible ? <ProductsBlockSettingsEditorDisplayOptions existing={ this.state.display ? true : false } update_display_callback={ this.updateDisplay } /> : null; const menu = this.state.menu_visible ? <ProductsBlockSettingsEditorDisplayOptions existing={ this.state.display ? true : false } update_display_callback={ this.updateDisplay } /> : null;

View File

@ -1,17 +1,282 @@
const { __ } = wp.i18n; const { __ } = wp.i18n;
const { Toolbar, withAPIData, Dropdown } = wp.components; const { Toolbar, withAPIData, Dropdown } = wp.components;
/**
* Attribute data cache. Needed because it takes a lot of API calls to generate attribute info.
*/
const PRODUCT_ATTRIBUTE_DATA = {};
/** /**
* When the display mode is 'Attribute' search for and select product attributes to pull products from. * When the display mode is 'Attribute' search for and select product attributes to pull products from.
*/ */
export class ProductsAttributeSelect extends React.Component { export class ProductsAttributeSelect extends React.Component {
/**
* Constructor.
*/
constructor( props ) {
super( props );
this.state = {
selectedAttribute: '',
selectedTerms: [],
filterQuery: '',
}
this.setSelectedAttribute = this.setSelectedAttribute.bind( this );
this.addTerm = this.addTerm.bind( this );
this.removeTerm = this.removeTerm.bind( this );
}
/**
* Set the selected attribute.
*
* @param slug string Attribute slug.
*/
setSelectedAttribute( slug ) {
this.setState( {
selectedAttribute: slug,
selectedTerms: [],
} );
}
/**
* Add a term to the selected attribute's terms.
*
* @param slug string Term slug.
*/
addTerm( slug ) {
let terms = this.state.selectedTerms;
terms.push( slug );
this.setState( {
selectedTerms: terms,
} );
}
/**
* Remove a term from the selected attribute's terms.
*
* @param slug string Term slug.
*/
removeTerm( slug ) {
let newTerms = [];
for ( let termSlug of this.state.selectedTerms ) {
if ( termSlug !== slug ) {
newTerms.push( termSlug );
}
}
this.setState( {
selectedTerms: newTerms,
} );
}
/**
* Render the whole section.
*/
render() { render() {
// @todo Remove this once data is moving around properly.
console.log( "STATE UPDATED" );
console.log( this.state );
return ( return (
<div className="product-attribute-select"> <div className="product-attribute-select">
TODO: Attribute select screen <ProductAttributeFilter />
<ProductAttributeList
selectedAttribute={ this.state.selectedAttribute }
selectedTerms={ this.state.selectedTerms }
setSelectedAttribute={ this.setSelectedAttribute.bind( this ) }
addTerm={ this.addTerm.bind( this ) }
removeTerm={ this.removeTerm.bind( this ) }
/>
</div> </div>
); );
} }
} }
/**
* Search area for filtering through the attributes list.
*/
const ProductAttributeFilter = () => {
return (
<div>
<input id="product-attribute-search" type="search" placeholder={ __( 'Search for attributes' ) } />
</div>
);
}
/**
* List of attributes.
*/
const ProductAttributeList = withAPIData( ( props ) => {
return {
attributes: '/wc/v2/products/attributes'
};
} )( ( { attributes, selectedAttribute, selectedTerms, setSelectedAttribute, addTerm, removeTerm } ) => {
if ( ! attributes.data ) {
return __( 'Loading' );
}
if ( 0 === attributes.data.length ) {
return __( 'No attributes found' );
}
let attributeElements = [];
for ( let attribute of attributes.data ) {
if ( PRODUCT_ATTRIBUTE_DATA.hasOwnProperty( attribute.slug ) ) {
attributeElements.push( <ProductAttributeElement
selectedAttribute={ selectedAttribute }
selectedTerms={ selectedTerms }
attribute={attribute}
setSelectedAttribute={ setSelectedAttribute }
addTerm={ addTerm }
removeTerm={ removeTerm }
/> );
} else {
attributeElements.push( <UncachedProductAttributeElement
selectedAttribute={ selectedAttribute }
selectedTerms={ selectedTerms }
attribute={ attribute }
setSelectedAttribute={ setSelectedAttribute }
addTerm={ addTerm }
removeTerm={ removeTerm }
/> );
}
}
return (
<div className="product-attributes-list">
{ attributeElements }
</div>
);
}
);
/**
* Caches then renders a product attribute term element.
*/
const UncachedProductAttributeElement = withAPIData( ( props ) => {
return {
terms: '/wc/v2/products/attributes/' + props.attribute.id + '/terms'
};
} )( ( { terms, selectedAttribute, selectedTerms, attribute, setSelectedAttribute, addTerm, removeTerm } ) => {
if ( ! terms.data ) {
return __( 'Loading' );
}
if ( 0 === terms.data.length ) {
return __( 'No attribute options found' );
}
// Populate cache.
PRODUCT_ATTRIBUTE_DATA[ attribute.slug ] = { terms: [] };
let totalCount = 0;
for ( let term of terms.data ) {
totalCount += term.count;
PRODUCT_ATTRIBUTE_DATA[ attribute.slug ].terms.push( term );
}
PRODUCT_ATTRIBUTE_DATA[ attribute.slug ].count = totalCount;
return <ProductAttributeElement
selectedAttribute={ selectedAttribute }
selectedTerms={ selectedTerms }
attribute={ attribute }
setSelectedAttribute={ setSelectedAttribute }
addTerm={ addTerm }
removeTerm={ removeTerm }
/>
}
);
/**
* A product attribute term element.
*/
class ProductAttributeElement extends React.Component {
/**
* Constructor.
*/
constructor( props ) {
super( props );
this.handleAttributeChange = this.handleAttributeChange.bind( this );
this.handleTermChange = this.handleTermChange.bind( this );
}
/**
* Propagate and reset values when the selected attribute is changed.
*
* @param evt Event object
*/
handleAttributeChange( evt ) {
const slug = evt.target.value;
if ( this.props.selectedAttribute === slug ) {
return;
}
this.props.setSelectedAttribute( slug );
}
/**
* Add or remove selected terms.
*
* @param evt Event object
*/
handleTermChange( evt ) {
if ( evt.target.checked ) {
this.props.addTerm( evt.target.value );
} else {
this.props.removeTerm( evt.target.value );
}
}
/**
* Render the details for one attribute.
*/
render() {
const attribute = PRODUCT_ATTRIBUTE_DATA[ this.props.attribute.slug ];
const isSelected = this.props.selectedAttribute === this.props.attribute.slug;
let attributeTerms = null;
if ( isSelected ) {
attributeTerms = (
<ul className="product-attribute-terms">
{ attribute.terms.map( ( term ) => (
<li className="product-attribute-term">
<label>
<input type="checkbox"
value={ term.slug }
onChange={ this.handleTermChange }
checked={ this.props.selectedTerms.includes( term.slug ) }
/>
{ term.name }
<span className="product-attribute-count">{ term.count }</span>
</label>
</li>
) ) }
</ul>
);
}
return (
<div className="product-attribute">
<div className="product-attribute-name">
<label>
<input type="radio"
value={ this.props.attribute.slug }
onClick={ this.handleAttributeChange }
checked={ isSelected }
/>
{ this.props.attribute.name }
<span className="product-attribute-count">{ attribute.count }</span>
</label>
</div>
{ attributeTerms }
</div>
);
}
}