diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index d8581f333f..02310c9f49 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -693,18 +693,101 @@ h2.ep-list-features { * Weighting */ -.weighting-settings .postbox { +.weighting-settings { box-sizing: border-box; max-width: 650px; - & * { - box-sizing: border-box; + & .postbox { + + & .components-range-control__number { + display: none; + } + + & .postbox__header { + align-items: center; + border-bottom: 1px solid #c3c4c7; + display: flex; + } + + & .undo-changes { + background-color: transparent; + border: 0; + color: #1e8cbe; + cursor: pointer; + margin-left: auto; + padding: 0 10px; + + &:hover { + text-decoration: underline; + } + } + + & * { + box-sizing: border-box; + } + } + + & .add-meta-key-wrap { + align-items: center; + display: flex; + + & legend { + top: 0; + } + } + + & .add-meta-key { + width: 300px; + } + + & .submit__button { + align-items: center; + background: #fff; + bottom: 0; + display: flex; + padding: 10px; + position: sticky; + + & .note { + margin-left: 10px; + } + + & .reset-all-changes-button { + margin-left: auto; + } } + +} + +.components-range-control__wrapper { + min-width: 100px; + + & span:last-child { + background-color: #1e8cbe; + pointer-events: none; + } +} + +div.components-base-control__field { + align-items: center; + display: flex; + height: 20px; + margin: 0; + + & .components-base-control__label { + margin: 0; + } +} + +.field-item .components-base-control__help { + margin-top: -10px; + padding: 0; } .weighting-settings .postbox h2.hndle { color: #444; cursor: inherit; + width: 110px; } .weighting-settings fieldset { @@ -737,9 +820,26 @@ h2.ep-list-features { background: #f9f9f9; } -.weighting-settings .searchable { +.field-item { + align-items: center; + display: flex; +} + +.remove-meta-item { + cursor: pointer; + + & path { + fill: #d84440; + } +} + +.field-item p { display: inline-block; - width: 120px; + padding: 0 10px; + + & a { + text-decoration: none; + } } .weighting-settings .weighting { @@ -748,7 +848,6 @@ h2.ep-list-features { } .weighting-settings .weighting label { - margin-right: 10px; min-width: 80px; } @@ -759,7 +858,7 @@ h2.ep-list-features { height: 1em; margin: 0; vertical-align: middle; - width: 200px; + width: 120px; } .weighting-settings input[type="range"]:focus { diff --git a/assets/js/weighting/components/field-group.js b/assets/js/weighting/components/field-group.js new file mode 100644 index 0000000000..57777abce8 --- /dev/null +++ b/assets/js/weighting/components/field-group.js @@ -0,0 +1,181 @@ +/** + * Wordpress Dependecies. + */ +import { useState } from '@wordpress/element'; +import AsyncSelect from 'react-select/async'; +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal Dependencies. + */ +import FieldItem from './field-item'; +import MetaItem from './meta-Item'; +import { sortListByOrder } from '../utils'; + +const FieldGroup = ({ + postType, + onChangeHandler, + getCurrentFormData, + formData, + setFormData, + undoHandler, + savedData, +}) => { + const [selectedValue, setSelectedValue] = useState(null); + + // handle selection + const handleChange = (value) => { + let currentFormData = getCurrentFormData(postType.name); + if (currentFormData.metas.findIndex((metaItem) => metaItem.name === value.name) === -1) { + currentFormData = { + ...currentFormData, + metas: [...currentFormData.metas, value], + }; + } + const excludedFormData = formData.filter((item) => item.name !== postType.name); + const newFormData = [...excludedFormData, currentFormData]; + setFormData(sortListByOrder(newFormData)); + setSelectedValue(value); + }; + + // load options using API call + const loadOptions = (inputValue) => { + return fetch( + `https://jsonplaceholder.typicode.com/posts?search=${inputValue}$post_type=${postType.name}`, + ) + .then((res) => res.json()) + .then((result) => { + const newResult = []; + result.forEach((item) => { + newResult.push({ + name: item.title, + searchable: false, + weight: 10, + }); + }); + return newResult; + }); + }; + + const removeMetaItem = (metaName) => { + const newCurrentFormData = { + ...getCurrentFormData(postType.name), + metas: getCurrentFormData(postType.name).metas.filter((item) => item.name !== metaName), + }; + + const excludedFormData = formData.filter((item) => item.name !== postType.name); + const newFormData = [...excludedFormData, newCurrentFormData]; + setFormData(sortListByOrder(newFormData)); + }; + + const currentOriginalData = savedData.find((item) => item.name === postType.name); + + return ( + <> +
+

{__('Attributes', 'elasticpress')}

+
+ {postType.attributes.map((attribute, index) => { + const fieldType = 'attributes'; + const isAttributeChanged = + JSON.stringify(attribute) !== + JSON.stringify(currentOriginalData[fieldType][index]); + return ( + + ); + })} +
+
+
+

{__('Taxonomies', 'elasticpress')}

+
+ {postType.taxonomies.map((taxonomy, index) => { + const fieldType = 'taxonomies'; + const isAttributeChanged = + JSON.stringify(taxonomy) !== + JSON.stringify(currentOriginalData[fieldType][index]); + + return ( + + ); + })} +
+
+
+

{__('Meta to be indexed', 'elasticpress')}

+
+ {getCurrentFormData(postType.name) && + getCurrentFormData(postType.name).metas && + getCurrentFormData(postType.name).metas.map((meta, index) => { + const fieldType = 'metas'; + const isAttributeChanged = + JSON.stringify(meta) !== + JSON.stringify(currentOriginalData[fieldType][index]); + return ( + + ); + })} +
+ {__('Add Meta Key:', 'elasticpress')} +
+ e.name} + getOptionValue={(e) => e.name} + loadOptions={loadOptions} + // onInputChange={handleInputChange} + onChange={handleChange} + /> +
+
+
+
+ + ); +}; + +FieldGroup.propTypes = { + postType: PropTypes.object.isRequired, + onChangeHandler: PropTypes.func.isRequired, + getCurrentFormData: PropTypes.func.isRequired, + formData: PropTypes.array.isRequired, + setFormData: PropTypes.func.isRequired, + undoHandler: PropTypes.func.isRequired, + savedData: PropTypes.array.isRequired, +}; + +export default FieldGroup; diff --git a/assets/js/weighting/components/field-item.js b/assets/js/weighting/components/field-item.js new file mode 100644 index 0000000000..4e5c8baf45 --- /dev/null +++ b/assets/js/weighting/components/field-item.js @@ -0,0 +1,102 @@ +/** + * Wordpress Dependencies. + */ +import { RangeControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal Dependecies. + */ +import UndoChanges from './undo-changes'; + +const FieldItem = (props) => { + const { + postType, + attribute, + fieldType, + onChangeHandler, + getCurrentFormData, + currentIndex, + undoHandler, + isAttributeChanged, + } = props; + + return ( +
+ {attribute.label} + +

+ +

+ + {getCurrentFormData(postType.name)[fieldType][currentIndex].indexable && ( +

+ +

+ )} + + {getCurrentFormData(postType.name)[fieldType][currentIndex].searchable && + getCurrentFormData(postType.name)[fieldType][currentIndex].indexable && ( +

+ + onChangeHandler(value, postType.name, fieldType, attribute.name) + } + min={1} + max={100} + /> +

+ )} + + {isAttributeChanged && } +
+ ); +}; + +FieldItem.propTypes = { + postType: PropTypes.object.isRequired, + attribute: PropTypes.object.isRequired, + fieldType: PropTypes.string.isRequired, + onChangeHandler: PropTypes.func.isRequired, + getCurrentFormData: PropTypes.func.isRequired, + currentIndex: PropTypes.number.isRequired, + undoHandler: PropTypes.func.isRequired, + isAttributeChanged: PropTypes.bool.isRequired, +}; + +export default FieldItem; diff --git a/assets/js/weighting/components/meta-Item.js b/assets/js/weighting/components/meta-Item.js new file mode 100644 index 0000000000..877dae5a00 --- /dev/null +++ b/assets/js/weighting/components/meta-Item.js @@ -0,0 +1,72 @@ +/** + * WordPress dependencies. + */ +import { RangeControl } from '@wordpress/components'; +import { closeSmall, Icon } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +const MetaItem = ({ + postType, + attribute, + fieldType, + onChangeHandler, + getCurrentFormData, + currentIndex, + removeMetaItem, +}) => { + return ( +
+ + removeMetaItem(attribute.name)} icon={closeSmall} /> + + {attribute.name} + +

+ +

+ + {getCurrentFormData(postType.name)[fieldType][currentIndex].searchable && ( +

+ + onChangeHandler(value, postType.name, fieldType, attribute.name) + } + min={1} + max={100} + /> +

+ )} +
+ ); +}; + +MetaItem.propTypes = { + postType: PropTypes.object.isRequired, + attribute: PropTypes.object.isRequired, + fieldType: PropTypes.string.isRequired, + onChangeHandler: PropTypes.func.isRequired, + getCurrentFormData: PropTypes.func.isRequired, + currentIndex: PropTypes.number.isRequired, + removeMetaItem: PropTypes.func.isRequired, +}; + +export default MetaItem; diff --git a/assets/js/weighting/components/undo-changes.js b/assets/js/weighting/components/undo-changes.js new file mode 100644 index 0000000000..689d88147a --- /dev/null +++ b/assets/js/weighting/components/undo-changes.js @@ -0,0 +1,20 @@ +/** + * Wordpress Dependencies. + */ +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +const UndoChanges = ({ undoHandler, undoProps }) => { + return ( + + ); +}; + +UndoChanges.propTypes = { + undoHandler: PropTypes.func.isRequired, + undoProps: PropTypes.object.isRequired, +}; + +export default UndoChanges; diff --git a/assets/js/weighting/dummyData.js b/assets/js/weighting/dummyData.js new file mode 100644 index 0000000000..98e5482731 --- /dev/null +++ b/assets/js/weighting/dummyData.js @@ -0,0 +1,224 @@ +export const dummyData = [ + { + label: 'Posts', + name: 'post', + indexable: true, + order: 0, + + attributes: [ + { + label: 'Title', + name: 'post_title', + indexable: true, + searchable: true, + weight: 40, + }, + { + label: 'Content', + name: 'post_content', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Excerpt', + name: 'post_excerpt', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Author', + name: 'post_author', + indexable: false, + searchable: false, + weight: 1, + }, + ], + + taxonomies: [ + { + label: 'Categories', + name: 'post_categories', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Tags', + name: 'post_tags', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Formats', + name: 'post_formats', + indexable: false, + searchable: false, + weight: 1, + }, + ], + + metas: [ + // { + // name: 'example_key', + // searchable: false, + // weight: 10, + // }, + // { + // name: 'another_key', + // searchable: false, + // weight: 10, + // }, + // { + // name: 'one_more_key', + // searchable: false, + // weight: 10, + // }, + ], + }, + { + label: 'Pages', + name: 'page', + indexable: true, + order: 1, + + attributes: [ + { + label: 'Title', + name: 'post_title', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Content', + name: 'post_content', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Excerpt', + name: 'post_excerpt', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Author', + name: 'post_author', + indexable: false, + searchable: false, + weight: 1, + }, + ], + + taxonomies: [ + { + label: 'Categories', + name: 'post_categories', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Tags', + name: 'post_tags', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Formats', + name: 'post_formats', + indexable: false, + searchable: false, + weight: 1, + }, + ], + + metas: [], + }, + { + label: 'Product', + name: 'product', + indexable: true, + order: 2, + + attributes: [ + { + label: 'Title', + name: 'post_title', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Content', + name: 'post_content', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Excerpt', + name: 'post_excerpt', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Author', + name: 'post_author', + indexable: false, + searchable: false, + weight: 1, + }, + ], + + taxonomies: [ + { + label: 'Categories', + name: 'post_categories', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Tags', + name: 'post_tags', + indexable: false, + searchable: false, + weight: 1, + }, + { + label: 'Formats', + name: 'post_formats', + indexable: false, + searchable: false, + weight: 1, + }, + ], + metas: [], + }, +]; + +export const dummyMetaKeys = [ + { + name: 'example_key', + searchable: false, + weight: 10, + }, + { + name: 'another_key', + searchable: false, + weight: 10, + }, + { + name: 'one_more_key', + searchable: false, + weight: 10, + }, +]; diff --git a/assets/js/weighting/index.js b/assets/js/weighting/index.js new file mode 100644 index 0000000000..57c1940568 --- /dev/null +++ b/assets/js/weighting/index.js @@ -0,0 +1,245 @@ +/** + * WordPress dependencies. + */ +import { render, WPElement, useState, useEffect } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal Dependencies. + */ +import FieldGroup from './components/field-group'; +import { dummyData } from './dummyData'; +import { sortListByOrder } from './utils'; + +/** + * component. + * + * @return {WPElement} Element. + */ +const App = () => { + const [formData, setFormData] = useState([]); + const [savedData, setSavedData] = useState([]); + const [isSaving, setIsSaving] = useState(false); + + const hasChanges = JSON.stringify(savedData) !== JSON.stringify(formData); + + /** + * Fetch api response on component mount and set instead of dummy data. + */ + useEffect(() => { + setTimeout(() => { + setSavedData(dummyData); + setFormData(dummyData); + }, 200); + }, []); + + /** + * Get currently editing form type + * + * @param {string} postType type of the wp post. + * @return {Object} currently editing form. + */ + const getCurrentFormData = (postType) => formData.find((item) => item.name === postType); + + /** + * Get currently editing form type + * + * @param {string} postType type of the wp post. + * @return {Object} currently editing form. + */ + const getSavedFormData = (postType) => savedData.find((item) => item.name === postType); + + const undoHandler = (props) => { + const { currentIndex, fieldType, postType, attribute } = props; + let currentFormData = getCurrentFormData(postType.name); + const savedFormData = getSavedFormData(postType.name); + + currentFormData = { + ...currentFormData, + [fieldType]: currentFormData[fieldType].map((item) => { + let newItem = item; + if (item.name === attribute.name) { + newItem = savedFormData[fieldType][currentIndex]; + } + return newItem; + }), + }; + + const excludeOldCurrentFormData = formData.filter((item) => item.name !== postType.name); + + const newFormData = [...excludeOldCurrentFormData, currentFormData]; + + setFormData(sortListByOrder(newFormData)); + }; + + /** + * Handle input changes. + * + * @param {Object} event react synthetic event + * @param {string} postType wp post type + * @param {string} type type of the field + * @param {string} attributeName field attribute name + */ + const onChangeHandler = (event, postType, type, attributeName) => { + let currentFormData = getCurrentFormData(postType); + + if (type === 'root') { + currentFormData = { ...currentFormData, indexable: !currentFormData.indexable }; + } else { + currentFormData = { + ...currentFormData, + [type]: currentFormData[type].map((item) => { + let newItem = item; + if (typeof event === 'number' && item.name === attributeName) { + newItem = { ...newItem, weight: event }; + } else if (item.name === attributeName) { + newItem = { ...newItem, [event.target.name]: !newItem[event.target.name] }; + } + return newItem; + }), + }; + } + + const excludeOldCurrentFormData = formData.filter((item) => item.name !== postType); + + const newFormData = [...excludeOldCurrentFormData, currentFormData]; + + setFormData(sortListByOrder(newFormData)); + }; + + /** + * Reset all changes of the form. + */ + const resetAllChanges = () => { + setFormData(sortListByOrder(dummyData)); + }; + + /** + * Reset current form changes. + * + * @param {string} postType name of the post type. + */ + const resetCurrentFormChanges = (postType) => { + const savedFormData = getSavedFormData(postType); + const excludeOldCurrentFormData = formData.filter((item) => item.name !== postType); + + const newFormData = [...excludeOldCurrentFormData, savedFormData]; + + setFormData(sortListByOrder(newFormData)); + }; + + /** + * Check if current has changes. + * + * @param {string} postType name of the post type. + */ + const hasCurrentFormChanges = (postType) => + JSON.stringify(getCurrentFormData(postType)) !== JSON.stringify(getSavedFormData(postType)); + + /** + * Handle for submission. + * + * @param {Object} event react synthetic event. + */ + const handleSubmit = (event) => { + event.preventDefault(); + setIsSaving(true); + + // do api request to save formData + setTimeout(() => { + console.log({ formData }); + }, 500); + + setIsSaving(false); + }; + + return ( +
+

{__('Manage Search Fields & Weighting', 'elasticpress')}

+
+

+ {__( + 'Adding more weight to an item will mean it will have more presence during searches. Add more weight to the items that are more important and need more prominence during searches. For example, adding more weight to the title attribute will cause search matches on the post title to apear mor prominently.', + 'elasticpress', + )} +

+

+ {__( + 'Important: If you enable or disable indexing for a field, you will need to refresh your index after saving your settings', + 'elasticpress', + )} +

+
+ {formData.map((postType) => ( +
+
+

{postType.label}

+
+ +
+ + {hasCurrentFormChanges(postType.name) && ( + + )} +
+ + {postType.indexable && ( + + )} +
+ ))} + +
+ + + + {hasChanges + ? __('Please re-sync your data after saving.', 'elasticpress') + : __('You have nothing to save.', 'elasticpress')} + + + +
+
+ ); +}; + +render(, document.getElementById('ep-weighting-screen')); diff --git a/assets/js/weighting/utils.js b/assets/js/weighting/utils.js new file mode 100644 index 0000000000..7b0847f5b3 --- /dev/null +++ b/assets/js/weighting/utils.js @@ -0,0 +1,7 @@ +/** + * Sort list by order. + * + * @param {Array} list items to bo sorted. + * @return {Array} sorted list by it's order. + */ +export const sortListByOrder = (list = []) => list.sort((a, b) => a.order - b.order); diff --git a/includes/classes/Feature/Search/Weighting.php b/includes/classes/Feature/Search/Weighting.php index b053182b62..783118c615 100644 --- a/includes/classes/Feature/Search/Weighting.php +++ b/includes/classes/Feature/Search/Weighting.php @@ -40,6 +40,32 @@ public function setup() { add_filter( 'ep_query_weighting_fields', [ $this, 'adjust_weight_for_cross_fields' ], 10, 5 ); } + /** + * Returns unique list of meta keys + * + * @param string $post_type Post type + * + * @return array + */ + public function get_meta_keys_for_post_type( $post_type ) { + + + $meta_keys = array(); + $posts = get_posts( array( 'post_type' => $post_type, 'limit' => -1 ) ); + + + foreach ( $posts as $post ) { + $post_meta_keys = get_post_custom_keys( $post->ID ); + var_dump($post_meta_keys); + $meta_keys = array_merge( $meta_keys, $post_meta_keys ); + } + + // Use array_unique to remove duplicate meta_keys that we received from all posts + // Use array_values to reset the index of the array + return array_values( array_unique( $meta_keys ) ); + + } + /** * Returns a grouping of all the fields that support weighting for the post type * @@ -211,58 +237,7 @@ public function add_weighting_submenu_page() { public function render_settings_page() { include EP_PATH . '/includes/partials/header.php'; ?>
- -

-

-

- -
- - - -
-

-
- -
-

-
- get_registered_feature( 'search' ); - - $post_types = $search->get_searchable_post_types(); - - $current_values = $this->get_weighting_configuration(); - - foreach ( $post_types as $post_type ) : - $fields = $this->get_weightable_fields_for_post_type( $post_type ); - $post_type_object = get_post_type_object( $post_type ); - ?> -
-

labels->menu_name ); ?>

- - render_settings_section( $post_type, $field_group, $current_values ); - endforeach; - ?> -
- -
+
-
+
+

+ id="" name="weighting[][][enabled]"> + +

+

- id="" name="weighting[][][enabled]"> - + id="" name="weighting[][][enabled]"> +

@@ -326,6 +306,8 @@ public function render_settings_section( $post_type, $field, $current_values ) { " name="weighting[][][weight]" >

+ +