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 (
+
+ );
+ })}
+
+
+
+ >
+ );
+};
+
+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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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'; ?>
-