From 6819f1d206259decf74094ae4a84c40be2a8a686 Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Thu, 9 Jan 2025 10:54:51 -0300
Subject: [PATCH 01/23] Search Templates feature
---
.babelrc | 5 +
.husky/pre-commit | 3 -
.../apps/search-templates-list.js | 70 +++
.../components/new-template-row.js | 60 ++
.../components/template-field.js | 80 +++
.../components/template-row.js | 152 +++++
assets/js/search-templates/config.js | 7 +
assets/js/search-templates/index.js | 46 ++
assets/js/search-templates/provider.js | 116 ++++
assets/js/search-templates/style.css | 3 +
assets/js/settings-screen/index.js | 72 +++
assets/js/settings-screen/style.css | 27 +
includes/classes/Feature/ExternalContent.php | 2 +-
includes/classes/Feature/SearchTemplates.php | 202 +++++++
includes/classes/REST/SearchTemplates.php | 191 +++++++
includes/functions/utils.php | 25 +
package-lock.json | 519 ++++++++++++++++--
package.json | 8 +-
18 files changed, 1550 insertions(+), 38 deletions(-)
create mode 100755 .babelrc
create mode 100644 assets/js/search-templates/apps/search-templates-list.js
create mode 100644 assets/js/search-templates/components/new-template-row.js
create mode 100644 assets/js/search-templates/components/template-field.js
create mode 100644 assets/js/search-templates/components/template-row.js
create mode 100644 assets/js/search-templates/config.js
create mode 100644 assets/js/search-templates/index.js
create mode 100644 assets/js/search-templates/provider.js
create mode 100644 assets/js/search-templates/style.css
create mode 100644 assets/js/settings-screen/index.js
create mode 100644 assets/js/settings-screen/style.css
create mode 100644 includes/classes/Feature/SearchTemplates.php
create mode 100644 includes/classes/REST/SearchTemplates.php
diff --git a/.babelrc b/.babelrc
new file mode 100755
index 0000000..48f7e45
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,5 @@
+{
+ "presets": [
+ "@10up/babel-preset-default"
+ ]
+}
diff --git a/.husky/pre-commit b/.husky/pre-commit
index d24fdfc..2312dc5 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
npx lint-staged
diff --git a/assets/js/search-templates/apps/search-templates-list.js b/assets/js/search-templates/apps/search-templates-list.js
new file mode 100644
index 0000000..c1f901e
--- /dev/null
+++ b/assets/js/search-templates/apps/search-templates-list.js
@@ -0,0 +1,70 @@
+/**
+ * WordPress Dependencies.
+ */
+import { createInterpolateElement, WPElement } from '@wordpress/element';
+import { Panel, Spinner } from '@wordpress/components';
+import { __, sprintf } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSearchTemplate } from '../provider';
+import TemplateRow from '../components/template-row';
+import NewTemplateRow from '../components/new-template-row';
+import { endpointExample, searchApiDocUrl } from '../config';
+
+/**
+ * Search Templates app.
+ *
+ * @returns {WPElement} App element.
+ */
+export default () => {
+ const { isLoading, templates } = useSearchTemplate();
+
+ return (
+ <>
+
+ {createInterpolateElement(
+ __(
+ 'Search templates are Elasticsearch queries stored in ElasticPress.io servers used by the Search API . Please note that all the API fields are still available for custom search templates. Your templates do not to differ in post types, offset, pagination arguments, or even filters, as for those you can still use query parameters. The templates can be used for searching in different fields or applying different scores, for instance.',
+ 'elasticpress-labs',
+ ),
+ { a: }, // eslint-disable-line jsx-a11y/anchor-has-content, jsx-a11y/control-has-associated-label
+ )}
+
+
+ {createInterpolateElement(
+ sprintf(
+ __(
+ 'Once you have a search template saved, you can start sending requests to your endpoint URL below. Your template needs to have {{ep_placeholder}}
in all places where the search term needs to be used.',
+ 'elasticpress-labs',
+ ),
+ endpointExample,
+ ),
+ { code:
},
+ )}
+
+
+ {createInterpolateElement(
+ sprintf(__('Endpoint URL: %s
'), endpointExample),
+ { strong: , code:
},
+ )}
+
+
+ {isLoading ? (
+
+ {__('Loading...', 'elasticpress-labs')}
+
+
+ ) : (
+ <>
+ {Object.keys(templates).map((templateName) => (
+
+ ))}
+
+ >
+ )}
+
+ >
+ );
+};
diff --git a/assets/js/search-templates/components/new-template-row.js b/assets/js/search-templates/components/new-template-row.js
new file mode 100644
index 0000000..169ba1f
--- /dev/null
+++ b/assets/js/search-templates/components/new-template-row.js
@@ -0,0 +1,60 @@
+/**
+ * WordPress Dependencies.
+ */
+import { Button, Flex, PanelBody, PanelRow, TextControl } from '@wordpress/components';
+import { useState, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSearchTemplateDispatch } from '../provider';
+import TemplateField from './template-field';
+
+/**
+ * New Template Row component.
+ *
+ * @returns {WPElement}
+ */
+export default () => {
+ const [name, setName] = useState('');
+ const [template, setTemplate] = useState('');
+
+ const { saveTemplate } = useSearchTemplateDispatch();
+
+ const onAddNewTemplate = () => {
+ saveTemplate(name, template);
+ setName('');
+ setTemplate('');
+ };
+
+ return (
+
+
+
+
+
+
+
+ {__('Save Template', 'elasticpress-labs')}
+
+
+
+
+
+ );
+};
diff --git a/assets/js/search-templates/components/template-field.js b/assets/js/search-templates/components/template-field.js
new file mode 100644
index 0000000..330c8fc
--- /dev/null
+++ b/assets/js/search-templates/components/template-field.js
@@ -0,0 +1,80 @@
+/**
+ * WordPress Dependencies.
+ */
+import { BaseControl, Button, Flex, Notice } from '@wordpress/components';
+import { createInterpolateElement, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { defaultTemplate } from '../config';
+
+/**
+ * Template Field component.
+ *
+ * @param {object} props Component props.
+ * @param {string} props.value The search template as a string.
+ * @param {Function} props.onChange Function to be executed when the value changes.
+ * @param {boolean} props.disabled If the field should be enabled or not.
+ * @returns {WPElement}
+ */
+export default ({ value, onChange, disabled }) => {
+ const isValueValidJson = () => {
+ if (!value) {
+ return true;
+ }
+
+ try {
+ return JSON.parse(value) && !!value;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ return (
+ {{ep_placeholder}}, so it can be replaced by the actual search term.',
+ 'elasticpress-labs',
+ ),
+ { code:
},
+ )}
+ >
+ {isValueValidJson() || (
+
+ {__('This does not seem to be a valid JSON object.', 'elasticpress-labs')}
+
+ )}
+
+
+ {__('Template', 'elasticpress-labs')}
+
+ {defaultTemplate && (
+ {
+ onChange(JSON.stringify(defaultTemplate, null, '\t'));
+ }}
+ type="button"
+ variant="secondary"
+ >
+ {__('Import default template', 'elasticpress-labs')}
+
+ )}
+
+
+ );
+};
diff --git a/assets/js/search-templates/components/template-row.js b/assets/js/search-templates/components/template-row.js
new file mode 100644
index 0000000..b8b0a1b
--- /dev/null
+++ b/assets/js/search-templates/components/template-row.js
@@ -0,0 +1,152 @@
+/**
+ * WordPress Dependencies.
+ */
+import { Button, Flex, PanelBody, PanelRow, TextControl } from '@wordpress/components';
+import { useState, WPElement } from '@wordpress/element';
+import { cautionFilled, cloudDownload, cloudUpload } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { useSearchTemplate, useSearchTemplateDispatch } from '../provider';
+import { useSettingsScreen } from '../../settings-screen';
+import TemplateField from './template-field';
+
+/**
+ * Template Row component.
+ *
+ * @param {object} props Component props.
+ * @param {string} props.templateName The search template name.
+ * @returns {WPElement}
+ */
+export default ({ templateName }) => {
+ const { templates } = useSearchTemplate();
+ const { loadTemplate, deleteTemplate, saveTemplate } = useSearchTemplateDispatch();
+ const { createNotice } = useSettingsScreen();
+
+ const [template, setTemplate] = useState(
+ templates[templateName]
+ ? JSON.stringify(JSON.parse(templates[templateName]), null, '\t')
+ : '',
+ );
+ const [isSaving, setIsSaving] = useState(false);
+ const [icon, setIcon] = useState(null);
+
+ const onTogglePanelBody = (opening) => {
+ if (!opening) {
+ return;
+ }
+
+ if (templates[templateName] === null) {
+ setIcon(cloudDownload);
+ loadTemplate(templateName)
+ .then((response) => {
+ setTemplate(JSON.stringify(response, null, '\t'));
+ })
+ .catch((error) => {
+ createNotice(
+ 'error',
+ __('Could not load your templates.', 'elasticpress-labs'),
+ );
+ // eslint-disable-next-line no-console
+ console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
+ })
+ .finally(() => {
+ setIcon(null);
+ });
+ }
+ };
+
+ const onValueChange = (newValue) => {
+ setTemplate(newValue);
+
+ if (newValue !== JSON.stringify(JSON.parse(templates[templateName]), null, '\t')) {
+ setIcon(cautionFilled);
+ }
+ };
+
+ const onSaveTemplate = () => {
+ setIcon(cloudUpload);
+ setIsSaving(true);
+ saveTemplate(templateName, template)
+ .then((response) => {
+ setTemplate(JSON.stringify(response, null, '\t'));
+ setIcon(null);
+ createNotice('success', __('Template saved.', 'elasticpress-labs'));
+ })
+ .catch((error) => {
+ createNotice(
+ 'error',
+ __('Could not save the template. Please try again.', 'elasticpress-labs'),
+ );
+ // eslint-disable-next-line no-console
+ console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
+ })
+ .finally(() => {
+ setIcon(null);
+ setIsSaving(false);
+ });
+ };
+
+ const onDeleteTemplate = () => {
+ deleteTemplate(template).catch((error) => {
+ createNotice(
+ 'error',
+ __('Could not delete the template. Please try again.', 'elasticpress-labs'),
+ );
+ // eslint-disable-next-line no-console
+ console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ {__('Save changes', 'elasticpress-labs')}
+
+
+ {__('Delete template', 'elasticpress-labs')}
+
+
+
+
+
+ );
+};
diff --git a/assets/js/search-templates/config.js b/assets/js/search-templates/config.js
new file mode 100644
index 0000000..7d9fda4
--- /dev/null
+++ b/assets/js/search-templates/config.js
@@ -0,0 +1,7 @@
+/**
+ * Window dependencies.
+ */
+const { defaultTemplate, endpointExample, restApiEndpoint, searchApiDocUrl } =
+ window.epSearchTemplates;
+
+export { defaultTemplate, endpointExample, restApiEndpoint, searchApiDocUrl };
diff --git a/assets/js/search-templates/index.js b/assets/js/search-templates/index.js
new file mode 100644
index 0000000..bd80e43
--- /dev/null
+++ b/assets/js/search-templates/index.js
@@ -0,0 +1,46 @@
+/**
+ * WordPress dependencies.
+ */
+import { createRoot, render, WPElement } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies.
+ */
+import { SettingsScreenProvider } from '../settings-screen';
+import { SearchTemplatesProvider } from './provider';
+import SearchTemplatesList from './apps/search-templates-list';
+
+/**
+ * Styles.
+ */
+import './style.css';
+
+/**
+ * App component.
+ *
+ * @returns {WPElement}
+ */
+const App = () => (
+
+
+
+
+
+);
+
+/**
+ * Root element.
+ */
+const el = document.getElementById('ep-search-templates');
+
+/**
+ * Render.
+ */
+if (typeof createRoot === 'function') {
+ const root = createRoot(el);
+
+ root.render( );
+} else {
+ render( , el);
+}
diff --git a/assets/js/search-templates/provider.js b/assets/js/search-templates/provider.js
new file mode 100644
index 0000000..d9c9c95
--- /dev/null
+++ b/assets/js/search-templates/provider.js
@@ -0,0 +1,116 @@
+/**
+ * WordPress dependencies.
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { createContext, useContext, useEffect, useReducer, WPElement } from '@wordpress/element';
+
+/**
+ * Internal dependencies.
+ */
+import { restApiEndpoint } from './config';
+
+/**
+ * Sync contexts.
+ */
+const SearchTemplateContext = createContext();
+const SearchTemplateDispatchContext = createContext();
+
+/**
+ * Search Templates Provider.
+ *
+ * @param {object} props Component props.
+ * @param {Function} props.children Component children.
+ * @returns {WPElement} Element.
+ */
+export const SearchTemplatesProvider = ({ children }) => {
+ const reducer = (state, action) => {
+ switch (action.type) {
+ case 'SET_TEMPLATES':
+ return { ...state, isLoading: false, templates: action.templates };
+ case 'SET_TEMPLATE':
+ state.templates[action.templateName] = JSON.stringify(action.template);
+ return { ...state };
+ case 'DELETE_TEMPLATE':
+ delete state.templates[action.templateName];
+ return { ...state };
+ default:
+ return state;
+ }
+ };
+
+ const [state, dispatch] = useReducer(reducer, {
+ templates: {},
+ isLoading: true,
+ error: null,
+ });
+
+ const loadTemplate = async (template) => {
+ const response = await apiFetch({
+ path: `${restApiEndpoint}/${template}`,
+ });
+
+ dispatch({ type: 'SET_TEMPLATE', templateName: template, template: response });
+ return response;
+ };
+
+ const saveTemplate = async (name, template) => {
+ const response = apiFetch({
+ path: `${restApiEndpoint}/${name}`,
+ method: 'PUT',
+ body: template,
+ });
+
+ dispatch({ type: 'SET_TEMPLATE', templateName: name, template: response });
+ return response;
+ };
+
+ const deleteTemplate = (name) => {
+ const response = apiFetch({
+ path: `${restApiEndpoint}/${name}`,
+ method: 'DELETE',
+ });
+
+ dispatch({ type: 'DELETE_TEMPLATE', templateName: name });
+ return response;
+ };
+
+ useEffect(() => {
+ apiFetch({ path: restApiEndpoint }).then((response) => {
+ const templates = response.reduce((acc, template) => {
+ acc[template] = null;
+
+ return acc;
+ }, {});
+ dispatch({ type: 'SET_TEMPLATES', templates });
+ });
+ }, []);
+
+ // eslint-disable-next-line react/jsx-no-constructed-context-values
+ const customDispatch = {
+ deleteTemplate,
+ loadTemplate,
+ saveTemplate,
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+/**
+ * Use the Search Templates Context.
+ *
+ * @returns {object} Search Templates Context.
+ */
+export const useSearchTemplate = () => useContext(SearchTemplateContext);
+
+/**
+ * Use the Search Templates Dispatch Context.
+ *
+ * @returns {object} Search Templates Dispatch Context.
+ */
+export const useSearchTemplateDispatch = () => useContext(SearchTemplateDispatchContext);
diff --git a/assets/js/search-templates/style.css b/assets/js/search-templates/style.css
new file mode 100644
index 0000000..5c9262e
--- /dev/null
+++ b/assets/js/search-templates/style.css
@@ -0,0 +1,3 @@
+.ep-search-template-panel {
+ margin-bottom: 16px;
+}
diff --git a/assets/js/settings-screen/index.js b/assets/js/settings-screen/index.js
new file mode 100644
index 0000000..41f7473
--- /dev/null
+++ b/assets/js/settings-screen/index.js
@@ -0,0 +1,72 @@
+/**
+ * WordPress dependencies.
+ */
+import { createSlotFill, SlotFillProvider, SnackbarList } from '@wordpress/components';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { createContext, useContext, useMemo, WPElement } from '@wordpress/element';
+import { store as noticeStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies.
+ */
+import './style.css';
+
+const Context = createContext();
+const { Fill, Slot } = createSlotFill('SettingsPageAction');
+
+/**
+ * ElasticPress Settings Screen provider component.
+ *
+ * @param {object} props Component props.
+ * @param {WPElement} props.children Component children.
+ * @param {string} props.title Page title.
+ * @returns {WPElement} Sync page component.
+ */
+export const SettingsScreenProvider = ({ children, title }) => {
+ const { createNotice, removeNotice } = useDispatch(noticeStore);
+
+ const { notices } = useSelect((select) => {
+ return {
+ notices: select(noticeStore).getNotices(),
+ };
+ }, []);
+
+ const contextValue = useMemo(
+ () => ({
+ ActionSlot: Fill,
+ createNotice,
+ removeNotice,
+ }),
+ [createNotice, removeNotice],
+ );
+
+ return (
+
+
+
+
+
+ {children}
+
+
removeNotice(notice)}
+ />
+
+
+
+ );
+};
+
+/**
+ * Use the Settings Page.
+ *
+ * @returns {object} Settings Page Context.
+ */
+export const useSettingsScreen = () => {
+ return useContext(Context);
+};
diff --git a/assets/js/settings-screen/style.css b/assets/js/settings-screen/style.css
new file mode 100644
index 0000000..82d5634
--- /dev/null
+++ b/assets/js/settings-screen/style.css
@@ -0,0 +1,27 @@
+.ep-settings-page__wrap {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 800px;
+}
+
+.ep-settings-page__header {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+
+ & button {
+ margin-top: 5px;
+ }
+}
+
+.ep-settings-page__snackbar-list {
+ bottom: 40px;
+ left: 0;
+ padding: 0 16px;
+ position: fixed;
+
+ @media (min-width: 600px) {
+ left: auto;
+ padding: 0;
+ }
+}
diff --git a/includes/classes/Feature/ExternalContent.php b/includes/classes/Feature/ExternalContent.php
index 6f59f9c..db779a2 100644
--- a/includes/classes/Feature/ExternalContent.php
+++ b/includes/classes/Feature/ExternalContent.php
@@ -346,7 +346,7 @@ public function maybe_parse_js( $content, $path_or_url ) {
}
if ( 'remove_js_reserved_words' === $method ) {
- $content = str_replace( get_js_reserved_words(), '', $content );
+ $content = str_replace( \ElasticPressLabs\Utils\get_js_reserved_words(), '', $content );
}
}
return $content;
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
new file mode 100644
index 0000000..4425867
--- /dev/null
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -0,0 +1,202 @@
+slug = 'search_templates';
+
+ $this->title = esc_html__( 'Search Templates', 'elasticpress-labs' );
+
+ $this->summary = '' . sprintf(
+ /* translators: %s: Search API documentation URL */
+ __(
+ 'Search templates are Elasticsearch queries stored in ElasticPress.io servers used by the Search API .',
+ 'elasticpress-labs'
+ ),
+ $this->search_api_docs_url
+ ) . '
' .
+ '' . __( 'Please note that all the API fields are still available for custom search templates. Your templates do not to differ in post types, offset, pagination arguments, or even filters, as for those you can still use query parameters. The templates can be used for searching in different fields or applying different scores, for instance.', 'elasticpress-labs' ) . '
' .
+ '' . __( 'Requires an ElasticPress.io plan to function.', 'elasticpress' ) . '
';
+
+ parent::__construct();
+ }
+
+ /**
+ * Setup all feature hooks
+ */
+ public function setup() {
+ // Setup the UI.
+ add_action( 'admin_menu', [ $this, 'admin_menu' ], 50 );
+ add_action( 'admin_enqueue_scripts', [ $this, 'scripts' ] );
+
+ // Register REST routes.
+ add_action( 'rest_api_init', [ $this, 'setup_endpoint' ] );
+ }
+
+ /**
+ * Determine feature reqs status
+ *
+ * @return FeatureRequirementsStatus
+ */
+ public function requirements_status() {
+ $status_code = Utils\is_epio() ? 1 : 2;
+
+ $status = new FeatureRequirementsStatus( $status_code );
+
+ if ( 2 === $status_code ) {
+ $status->code = 2;
+ $status->message = esc_html__( 'You need an ElasticPress.io account to use this feature.', 'elasticpress-labs' );
+ }
+
+ return $status;
+ }
+
+ /**
+ * Is this our page.
+ *
+ * @return boolean
+ */
+ public function is_search_templates_page() {
+ if ( ! function_exists( '\get_current_screen' ) ) {
+ return false;
+ }
+
+ $screen = get_current_screen();
+ return ( 'elasticpress_page_elasticpress-search-templates' === $screen->base );
+ }
+
+ /**
+ * Adds the settings page to the admin menu.
+ *
+ * @return void
+ */
+ public function admin_menu() {
+ add_submenu_page(
+ 'elasticpress',
+ esc_html__( 'ElasticPress.io Search Templates', 'elasticpress-labs' ),
+ esc_html__( 'Search Templates', 'elasticpress-labs' ),
+ Utils\get_capability( 'search_templates' ),
+ 'elasticpress-search-templates',
+ [ $this, 'admin_page' ]
+ );
+ }
+
+ /**
+ * Enqueues scripts and styles.
+ *
+ * @return void
+ */
+ public function scripts() {
+ if ( ! $this->is_search_templates_page() ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'ep_search_templates_scripts',
+ ELASTICPRESS_LABS_URL . 'dist/js/search-templates-script.js',
+ LabsUtils\get_asset_info( 'search-templates-script', 'dependencies' ),
+ LabsUtils\get_asset_info( 'search-templates-script', 'version' ),
+ true
+ );
+
+ wp_set_script_translations( 'ep_search_templates_scripts', 'elasticpress-labs' );
+
+ wp_enqueue_style( 'wp-edit-post' );
+
+ wp_enqueue_style(
+ 'ep_synonyms_scripts',
+ ELASTICPRESS_LABS_URL . 'dist/css/search-templates-script.css',
+ [],
+ LabsUtils\get_asset_info( 'search-templates-script', 'version' ),
+ 'all'
+ );
+
+ $instant_results = \ElasticPress\Features::factory()->get_registered_feature( 'instant-results' );
+ $template = json_decode( $instant_results->epio_get_search_template() );
+
+ $index_name = \ElasticPress\Indexables::factory()->get( 'post' )->get_index_name();
+ $endpoint_example = Utils\get_host() . "/api/v1/search/posts/{$index_name}?search={search_term}&template_name={template}";
+
+ wp_localize_script(
+ 'ep_search_templates_scripts',
+ 'epSearchTemplates',
+ [
+ 'defaultTemplate' => $template,
+ 'endpointExample' => $endpoint_example,
+ 'searchApiDocUrl' => $this->search_api_docs_url,
+ 'restApiEndpoint' => 'elasticpress-labs/v1/search-templates',
+ ]
+ );
+ }
+
+ /**
+ * Setup REST endpoints
+ */
+ public function setup_endpoint() {
+ $controller = new \ElasticPressLabs\REST\SearchTemplates();
+ $controller->register_routes();
+ }
+
+ /**
+ * Renders the search templates page.
+ *
+ * @return void
+ */
+ public function admin_page() {
+ include EP_PATH . '/includes/partials/header.php';
+
+ ?>
+
+ settings_schema = [
+ [
+ 'key' => 'additional_links',
+ 'label' => sprintf(
+ '%2$s ',
+ esc_url( admin_url( 'admin.php?page=elasticpress-search-templates' ) ),
+ __( 'Manage search templates', 'elasticpress-labs' ),
+ ),
+ 'type' => 'markup',
+ ],
+ ];
+ }
+}
diff --git a/includes/classes/REST/SearchTemplates.php b/includes/classes/REST/SearchTemplates.php
new file mode 100644
index 0000000..638eb66
--- /dev/null
+++ b/includes/classes/REST/SearchTemplates.php
@@ -0,0 +1,191 @@
+ [ $this, 'get_search_templates' ],
+ 'methods' => 'GET',
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ],
+ );
+ register_rest_route(
+ 'elasticpress-labs/v1',
+ 'search-templates/(?P[\w-]+)',
+ [
+ 'args' => [
+ 'template_name' => [
+ 'description' => __( 'Template name.', 'elasticpress-labs' ),
+ 'type' => 'string',
+ 'sanitize_callback' => 'sanitize_text_field',
+ ],
+ ],
+ [
+ 'callback' => [ $this, 'get_search_template' ],
+ 'methods' => 'GET',
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ],
+ [
+ 'callback' => [ $this, 'update_search_template' ],
+ 'methods' => 'PUT',
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ],
+ [
+ 'callback' => [ $this, 'delete_search_template' ],
+ 'methods' => 'DELETE',
+ 'permission_callback' => [ $this, 'check_permission' ],
+ ],
+ ]
+ );
+ }
+
+ /**
+ * Check that the request has permission to manage search templates.
+ *
+ * @return boolean
+ */
+ public function check_permission() {
+ $capability = Utils\get_capability( 'search_templates' );
+
+ return current_user_can( $capability );
+ }
+
+ /**
+ * EP.io search templates endpoint.
+ *
+ * @return string
+ */
+ protected function get_search_templates_endpoint(): string {
+ return 'api/v1/search/posts/templates';
+ }
+
+ /**
+ * EP.io (single) search template endpoint.
+ *
+ * @return string
+ */
+ protected function get_search_template_endpoint(): string {
+ $index_name = \ElasticPress\Indexables::factory()->get( 'post' )->get_index_name();
+
+ return "api/v1/search/posts/{$index_name}/template";
+ }
+
+ /**
+ * List search templates handler.
+ *
+ * @return array|\WP_Error
+ */
+ public function get_search_templates() {
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request( $this->get_search_templates_endpoint() );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
+ }
+
+ $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
+ $index_name = \ElasticPress\Indexables::factory()->get( 'post' )->get_index_name();
+
+ return $response_body[ $index_name ] ?? [];
+ }
+
+ /**
+ * Get a single search template.
+ *
+ * @param \WP_REST_Request $request Full details about the request.
+ * @return object|\WP_Error
+ */
+ public function get_search_template( \WP_REST_Request $request ) {
+ $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request( $path );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
+ }
+
+ return json_decode( wp_remote_retrieve_body( $response ) );
+ }
+
+ /**
+ * Update a search template.
+ *
+ * @param \WP_REST_Request $request Full details about the request.
+ * @return object|\WP_Error
+ */
+ public function update_search_template( \WP_REST_Request $request ) {
+ $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request(
+ $path,
+ [
+ 'method' => 'PUT',
+ 'headers' => [
+ 'Content-Type' => 'application/json',
+ ],
+ 'body' => $request->get_body(),
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( 201 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
+ }
+
+ return json_decode( wp_remote_retrieve_body( $response ) );
+ }
+
+ /**
+ * Delete a search template.
+ *
+ * @param \WP_REST_Request $request Full details about the request.
+ * @return object|\WP_Error
+ */
+ public function delete_search_template( \WP_REST_Request $request ) {
+ $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request(
+ $path,
+ [
+ 'method' => 'DELETE',
+ ]
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ if ( 204 !== wp_remote_retrieve_response_code( $response ) ) {
+ return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
+ }
+
+ return wp_remote_retrieve_body( $response );
+ }
+}
diff --git a/includes/functions/utils.php b/includes/functions/utils.php
index af05ea8..172fa9f 100644
--- a/includes/functions/utils.php
+++ b/includes/functions/utils.php
@@ -5,6 +5,8 @@
* @package ElasticPressLabs
*/
+namespace ElasticPressLabs\Utils;
+
/**
* List of reserved words in JavaScript
*
@@ -217,3 +219,26 @@ function get_js_reserved_words() {
'jQuery',
];
}
+
+/**
+ * Get asset info from extracted asset files
+ *
+ * @param string $slug Asset slug as defined in build/webpack configuration
+ * @param string $attribute Optional attribute to get. Can be version or dependencies
+ * @return string|array
+ */
+function get_asset_info( $slug, $attribute = null ) {
+ if ( file_exists( ELASTICPRESS_LABS_PATH . 'dist/js/' . $slug . '.asset.php' ) ) {
+ $asset = require ELASTICPRESS_LABS_PATH . 'dist/js/' . $slug . '.asset.php';
+ } elseif ( file_exists( ELASTICPRESS_LABS_PATH . 'dist/css/' . $slug . '.asset.php' ) ) {
+ $asset = require ELASTICPRESS_LABS_PATH . 'dist/css/' . $slug . '.asset.php';
+ } else {
+ return null;
+ }
+
+ if ( ! empty( $attribute ) && isset( $asset[ $attribute ] ) ) {
+ return $asset[ $attribute ];
+ }
+
+ return $asset;
+}
diff --git a/package-lock.json b/package-lock.json
index 64d763d..b282127 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"name": "elasticpress-labs",
"version": "2.3.1",
"license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@wordpress/icons": "^10.15.1"
+ },
"devDependencies": {
"10up-toolkit": "^6.3.0",
"husky": "^9.1.7",
@@ -4791,6 +4794,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
+ "license": "MIT"
+ },
"node_modules/@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
@@ -4805,6 +4814,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/react": {
+ "version": "18.3.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
+ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
+ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
@@ -5378,6 +5406,63 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/@wordpress/element": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.15.1.tgz",
+ "integrity": "sha512-RTKQwu+sgpdemzMPa/PT6XF+YqFxHIMH2MVEnCsDwaEusPYNmjJ3Lu8oTkgP+iWn2mhI21M/Xkb9jCgXeTnTyQ==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@babel/runtime": "7.25.7",
+ "@types/react": "^18.2.79",
+ "@types/react-dom": "^18.2.25",
+ "@wordpress/escape-html": "^3.15.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/element/node_modules/@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@wordpress/escape-html": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.15.0.tgz",
+ "integrity": "sha512-m+bdBFMbii7Bm0q3L6ntVH0jr+/sUJC4sgpPuVpsjZTFrxGIiXa0J5Kv6lQoBCFM6I3zaVdjroUA24l7JVjXaA==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@babel/runtime": "7.25.7"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/escape-html/node_modules/@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@wordpress/eslint-plugin": {
"version": "17.13.0",
"resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.13.0.tgz",
@@ -5543,6 +5628,33 @@
"node": ">= 6"
}
},
+ "node_modules/@wordpress/icons": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.15.1.tgz",
+ "integrity": "sha512-62GlyUePiF7+AOUNWjVYQ6ghWtgdl0FU5Fl0wS1EKRP6oBFXJ1DdHgXKJImKSY72KC8Yk8uS6VfqIiK2tsOeGg==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.15.1",
+ "@wordpress/primitives": "^4.15.1"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ }
+ },
+ "node_modules/@wordpress/icons/node_modules/@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@wordpress/jest-console": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.29.0.tgz",
@@ -5573,6 +5685,36 @@
"prettier": ">=3"
}
},
+ "node_modules/@wordpress/primitives": {
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.15.1.tgz",
+ "integrity": "sha512-hLBgrnKoEjROuqqlRPjAI5A903LsnOCUnPgBJLuREXgSRFVmVJocRHmvD8o3HGSaaTLcHivfowUhhmdlVkeYfw==",
+ "license": "GPL-2.0-or-later",
+ "dependencies": {
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.15.1",
+ "clsx": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=18.12.0",
+ "npm": ">=8.19.2"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0"
+ }
+ },
+ "node_modules/@wordpress/primitives/node_modules/@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@wordpress/warning": {
"version": "2.58.0",
"resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.58.0.tgz",
@@ -6722,7 +6864,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"pascal-case": "^3.1.2",
@@ -6820,6 +6961,17 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/capital-case": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz",
+ "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case-first": "^2.0.2"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -6837,6 +6989,26 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/change-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz",
+ "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==",
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "capital-case": "^1.0.4",
+ "constant-case": "^3.0.4",
+ "dot-case": "^3.0.4",
+ "header-case": "^2.0.4",
+ "no-case": "^3.0.4",
+ "param-case": "^3.0.4",
+ "pascal-case": "^3.1.2",
+ "path-case": "^3.0.4",
+ "sentence-case": "^3.0.4",
+ "snake-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/char-regex": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -7042,6 +7214,15 @@
"node": ">=0.8"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -7230,6 +7411,17 @@
"node": "^14.18.0 || >=16.10.0"
}
},
+ "node_modules/constant-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz",
+ "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case": "^2.0.2"
+ }
+ },
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -7803,6 +7995,12 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -8259,7 +8457,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
@@ -10612,6 +10809,16 @@
"he": "bin/he"
}
},
+ "node_modules/header-case": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz",
+ "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==",
+ "license": "MIT",
+ "dependencies": {
+ "capital-case": "^1.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -11563,9 +11770,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12459,7 +12664,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -13285,7 +13489,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -13298,7 +13501,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.0.3"
@@ -13924,7 +14126,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"lower-case": "^2.0.2",
@@ -14406,7 +14607,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
@@ -14474,13 +14674,22 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
+ "node_modules/path-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz",
+ "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==",
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -16551,7 +16760,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
@@ -16560,6 +16768,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -16749,7 +16970,6 @@
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true,
"license": "MIT"
},
"node_modules/regenerator-transform": {
@@ -17176,6 +17396,15 @@
}
}
},
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -17316,6 +17545,17 @@
"node": ">= 0.8"
}
},
+ "node_modules/sentence-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz",
+ "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==",
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case-first": "^2.0.2"
+ }
+ },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -17720,7 +17960,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dot-case": "^3.0.4",
@@ -19169,7 +19408,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD"
},
"node_modules/tsutils": {
@@ -19459,6 +19697,24 @@
"browserslist": ">= 4.21.0"
}
},
+ "node_modules/upper-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz",
+ "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/upper-case-first": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz",
+ "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -23042,6 +23298,11 @@
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"dev": true
},
+ "@types/prop-types": {
+ "version": "15.7.14",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
+ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
+ },
"@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
@@ -23054,6 +23315,21 @@
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true
},
+ "@types/react": {
+ "version": "18.3.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
+ "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-dom": {
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
+ "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
+ "requires": {}
+ },
"@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
@@ -23463,6 +23739,49 @@
"json2php": "^0.0.7"
}
},
+ "@wordpress/element": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.15.1.tgz",
+ "integrity": "sha512-RTKQwu+sgpdemzMPa/PT6XF+YqFxHIMH2MVEnCsDwaEusPYNmjJ3Lu8oTkgP+iWn2mhI21M/Xkb9jCgXeTnTyQ==",
+ "requires": {
+ "@babel/runtime": "7.25.7",
+ "@types/react": "^18.2.79",
+ "@types/react-dom": "^18.2.25",
+ "@wordpress/escape-html": "^3.15.0",
+ "change-case": "^4.1.2",
+ "is-plain-object": "^5.0.0",
+ "react": "^18.3.0",
+ "react-dom": "^18.3.0"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "requires": {
+ "regenerator-runtime": "^0.14.0"
+ }
+ }
+ }
+ },
+ "@wordpress/escape-html": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.15.0.tgz",
+ "integrity": "sha512-m+bdBFMbii7Bm0q3L6ntVH0jr+/sUJC4sgpPuVpsjZTFrxGIiXa0J5Kv6lQoBCFM6I3zaVdjroUA24l7JVjXaA==",
+ "requires": {
+ "@babel/runtime": "7.25.7"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "requires": {
+ "regenerator-runtime": "^0.14.0"
+ }
+ }
+ }
+ },
"@wordpress/eslint-plugin": {
"version": "17.13.0",
"resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.13.0.tgz",
@@ -23565,6 +23884,26 @@
}
}
},
+ "@wordpress/icons": {
+ "version": "10.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-10.15.1.tgz",
+ "integrity": "sha512-62GlyUePiF7+AOUNWjVYQ6ghWtgdl0FU5Fl0wS1EKRP6oBFXJ1DdHgXKJImKSY72KC8Yk8uS6VfqIiK2tsOeGg==",
+ "requires": {
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.15.1",
+ "@wordpress/primitives": "^4.15.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "requires": {
+ "regenerator-runtime": "^0.14.0"
+ }
+ }
+ }
+ },
"@wordpress/jest-console": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.29.0.tgz",
@@ -23582,6 +23921,26 @@
"dev": true,
"requires": {}
},
+ "@wordpress/primitives": {
+ "version": "4.15.1",
+ "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.15.1.tgz",
+ "integrity": "sha512-hLBgrnKoEjROuqqlRPjAI5A903LsnOCUnPgBJLuREXgSRFVmVJocRHmvD8o3HGSaaTLcHivfowUhhmdlVkeYfw==",
+ "requires": {
+ "@babel/runtime": "7.25.7",
+ "@wordpress/element": "^6.15.1",
+ "clsx": "^2.1.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.25.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz",
+ "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==",
+ "requires": {
+ "regenerator-runtime": "^0.14.0"
+ }
+ }
+ }
+ },
"@wordpress/warning": {
"version": "2.58.0",
"resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.58.0.tgz",
@@ -24354,7 +24713,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
- "dev": true,
"requires": {
"pascal-case": "^3.1.2",
"tslib": "^2.0.3"
@@ -24412,6 +24770,16 @@
"integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==",
"dev": true
},
+ "capital-case": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz",
+ "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==",
+ "requires": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case-first": "^2.0.2"
+ }
+ },
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -24422,6 +24790,25 @@
"supports-color": "^7.1.0"
}
},
+ "change-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz",
+ "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==",
+ "requires": {
+ "camel-case": "^4.1.2",
+ "capital-case": "^1.0.4",
+ "constant-case": "^3.0.4",
+ "dot-case": "^3.0.4",
+ "header-case": "^2.0.4",
+ "no-case": "^3.0.4",
+ "param-case": "^3.0.4",
+ "pascal-case": "^3.1.2",
+ "path-case": "^3.0.4",
+ "sentence-case": "^3.0.4",
+ "snake-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"char-regex": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
@@ -24557,6 +24944,11 @@
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"dev": true
},
+ "clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
+ },
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -24700,6 +25092,16 @@
"integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==",
"dev": true
},
+ "constant-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz",
+ "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==",
+ "requires": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case": "^2.0.2"
+ }
+ },
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -25038,6 +25440,11 @@
}
}
},
+ "csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+ },
"damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -25330,7 +25737,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
- "dev": true,
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
@@ -26943,6 +27349,15 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
+ "header-case": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz",
+ "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==",
+ "requires": {
+ "capital-case": "^1.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -27532,9 +27947,7 @@
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
- "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
- "dev": true,
- "peer": true
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
},
"is-regex": {
"version": "1.2.0",
@@ -28158,8 +28571,7 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.14.1",
@@ -28681,7 +29093,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
@@ -28690,7 +29101,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
- "dev": true,
"requires": {
"tslib": "^2.0.3"
}
@@ -29098,7 +29508,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
- "dev": true,
"requires": {
"lower-case": "^2.0.2",
"tslib": "^2.0.3"
@@ -29423,7 +29832,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
- "dev": true,
"requires": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
@@ -29471,12 +29879,20 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
- "dev": true,
"requires": {
"no-case": "^3.0.4",
"tslib": "^2.0.3"
}
},
+ "path-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz",
+ "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==",
+ "requires": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -30620,11 +31036,19 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dev": true,
"requires": {
"loose-envify": "^1.1.0"
}
},
+ "react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ }
+ },
"react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -30756,8 +31180,7 @@
"regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"regenerator-transform": {
"version": "0.15.2",
@@ -31016,6 +31439,14 @@
"neo-async": "^2.6.2"
}
},
+ "scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
"schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -31125,6 +31556,16 @@
}
}
},
+ "sentence-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz",
+ "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==",
+ "requires": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3",
+ "upper-case-first": "^2.0.2"
+ }
+ },
"serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -31407,7 +31848,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz",
"integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==",
- "dev": true,
"requires": {
"dot-case": "^3.0.4",
"tslib": "^2.0.3"
@@ -32430,8 +32870,7 @@
"tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"tsutils": {
"version": "3.21.0",
@@ -32613,6 +33052,22 @@
"picocolors": "^1.1.0"
}
},
+ "upper-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz",
+ "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==",
+ "requires": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "upper-case-first": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz",
+ "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==",
+ "requires": {
+ "tslib": "^2.0.3"
+ }
+ },
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 5df8d48..a39f41d 100644
--- a/package.json
+++ b/package.json
@@ -39,8 +39,12 @@
},
"10up-toolkit": {
"entry": {
- "admin": "./assets/js/admin/admin.js"
+ "admin": "./assets/js/admin/admin.js",
+ "search-templates-script": "./assets/js/search-templates/index.js"
},
- "wpDependencyExternals": false
+ "wpDependencyExternals": true
+ },
+ "dependencies": {
+ "@wordpress/icons": "^10.15.1"
}
}
From 9a12791f6eacdcc6c9e1eeaefe634a6b6a5728c3 Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Thu, 9 Jan 2025 10:58:16 -0300
Subject: [PATCH 02/23] PHP Lint
---
includes/classes/Feature/SearchTemplates.php | 2 +-
includes/classes/REST/SearchTemplates.php | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
index 4425867..53cf5dc 100644
--- a/includes/classes/Feature/SearchTemplates.php
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -193,7 +193,7 @@ protected function set_settings_schema() {
'label' => sprintf(
'%2$s ',
esc_url( admin_url( 'admin.php?page=elasticpress-search-templates' ) ),
- __( 'Manage search templates', 'elasticpress-labs' ),
+ __( 'Manage search templates', 'elasticpress-labs' )
),
'type' => 'markup',
],
diff --git a/includes/classes/REST/SearchTemplates.php b/includes/classes/REST/SearchTemplates.php
index 638eb66..0aad442 100644
--- a/includes/classes/REST/SearchTemplates.php
+++ b/includes/classes/REST/SearchTemplates.php
@@ -27,7 +27,7 @@ public function register_routes() {
'callback' => [ $this, 'get_search_templates' ],
'methods' => 'GET',
'permission_callback' => [ $this, 'check_permission' ],
- ],
+ ]
);
register_rest_route(
'elasticpress-labs/v1',
From 14acb1277de34271a6f7bf02f32497aa5769601f Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Thu, 9 Jan 2025 12:23:13 -0300
Subject: [PATCH 03/23] Update the installation script and install svn in GH
Action
---
.github/workflows/test.yml | 5 +
bin/install-wp-tests.sh | 256 +++++++++++++++++++++----------------
2 files changed, 152 insertions(+), 109 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7e5b54b..14eaf34 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -54,6 +54,11 @@ jobs:
- name: Check ES response
run: curl --connect-timeout 5 --max-time 10 --retry 5 --retry-max-time 40 --retry-all-errors http://127.0.0.1:8890
+ - name: Install SVN ( Subversion )
+ run: |
+ sudo apt-get update
+ sudo apt-get install subversion
+
- name: Setup WP Tests
run: |
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1
diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh
index 641750d..d2605fe 100644
--- a/bin/install-wp-tests.sh
+++ b/bin/install-wp-tests.sh
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
if [ $# -lt 3 ]; then
- echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
- exit 1
+ echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
+ exit 1
fi
DB_NAME=$1
@@ -10,147 +10,185 @@ DB_USER=$2
DB_PASS=$3
DB_HOST=${4-localhost}
WP_VERSION=${5-latest}
-CREATE_DB_IF_EXISTS=${6-false}
+SKIP_DB_CREATE=${6-false}
TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
-WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
+WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress}
download() {
if [ `which curl` ]; then
curl -s "$1" > "$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
+ else
+ echo "Error: Neither curl nor wget is installed."
+ exit 1
+ fi
+}
+
+# Check if svn is installed
+check_svn_installed() {
+ if ! command -v svn > /dev/null; then
+ echo "Error: svn is not installed. Please install svn and try again."
+ exit 1
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
- WP_BRANCH=${WP_VERSION%\-*}
- WP_TESTS_TAG="branches/$WP_BRANCH"
+ WP_BRANCH=${WP_VERSION%\-*}
+ WP_TESTS_TAG="branches/$WP_BRANCH"
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
- WP_TESTS_TAG="branches/$WP_VERSION"
+ WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
- if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
- # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
- WP_TESTS_TAG="tags/${WP_VERSION%??}"
- else
- WP_TESTS_TAG="tags/$WP_VERSION"
- fi
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ WP_TESTS_TAG="tags/${WP_VERSION%??}"
+ else
+ WP_TESTS_TAG="tags/$WP_VERSION"
+ fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
- WP_TESTS_TAG="trunk"
+ WP_TESTS_TAG="trunk"
else
- # http serves a single offer, whereas https serves multiple. we only want one
- download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
- grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
- LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
- if [[ -z "$LATEST_VERSION" ]]; then
- echo "Latest WordPress version could not be found"
- exit 1
- fi
- WP_TESTS_TAG="tags/$LATEST_VERSION"
+ # http serves a single offer, whereas https serves multiple. we only want one
+ download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
+ grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
+ LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
+ if [[ -z "$LATEST_VERSION" ]]; then
+ echo "Latest WordPress version could not be found"
+ exit 1
+ fi
+ WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
set -ex
install_wp() {
- if [ -d $WP_CORE_DIR ]; then
- return;
- fi
-
- mkdir -p $WP_CORE_DIR
-
- if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
- mkdir -p $TMPDIR/wordpress-nightly
- download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
- unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
- mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
- else
- if [ $WP_VERSION == 'latest' ]; then
- local ARCHIVE_NAME='latest'
- elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
- # https serves multiple offers, whereas http serves single.
- download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
- if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
- # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
- LATEST_VERSION=${WP_VERSION%??}
- else
- # otherwise, scan the releases and get the most up to date minor version of the major release
- local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
- LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
- fi
- if [[ -z "$LATEST_VERSION" ]]; then
- local ARCHIVE_NAME="wordpress-$WP_VERSION"
- else
- local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
- fi
- else
- local ARCHIVE_NAME="wordpress-$WP_VERSION"
- fi
- download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
- tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
- fi
-
- download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
+ if [ -d $WP_CORE_DIR ]; then
+ return;
+ fi
+
+ mkdir -p $WP_CORE_DIR
+
+ if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
+ mkdir -p $TMPDIR/wordpress-trunk
+ rm -rf $TMPDIR/wordpress-trunk/*
+ check_svn_installed
+ svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
+ mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
+ else
+ if [ $WP_VERSION == 'latest' ]; then
+ local ARCHIVE_NAME='latest'
+ elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
+ # https serves multiple offers, whereas http serves single.
+ download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
+ if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
+ # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
+ LATEST_VERSION=${WP_VERSION%??}
+ else
+ # otherwise, scan the releases and get the most up to date minor version of the major release
+ local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
+ LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
+ fi
+ if [[ -z "$LATEST_VERSION" ]]; then
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ else
+ local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
+ fi
+ else
+ local ARCHIVE_NAME="wordpress-$WP_VERSION"
+ fi
+ download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
+ tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
+ fi
+
+ download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_test_suite() {
- # portable in-place argument for both GNU sed and Mac OSX sed
- if [[ $(uname -s) == 'Darwin' ]]; then
- local ioption='-i.bak'
- else
- local ioption='-i'
- fi
-
- # set up testing suite if it doesn't yet exist
- if [ ! -d $WP_TESTS_DIR ]; then
- # set up testing suite
- mkdir -p $WP_TESTS_DIR
- svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
- svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
- fi
-
- if [ ! -f wp-tests-config.php ]; then
- download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
- # remove all forward slashes in the end
- WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
- sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
- sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
- sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
- sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
- sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
- fi
+ # portable in-place argument for both GNU sed and Mac OSX sed
+ if [[ $(uname -s) == 'Darwin' ]]; then
+ local ioption='-i.bak'
+ else
+ local ioption='-i'
+ fi
+
+ # set up testing suite if it doesn't yet exist
+ if [ ! -d $WP_TESTS_DIR ]; then
+ # set up testing suite
+ mkdir -p $WP_TESTS_DIR
+ rm -rf $WP_TESTS_DIR/{includes,data}
+ check_svn_installed
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
+ svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
+ fi
+
+ if [ ! -f wp-tests-config.php ]; then
+ download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
+ # remove all forward slashes in the end
+ WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
+ sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
+ sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
+ fi
}
-install_db() {
+recreate_db() {
+ shopt -s nocasematch
+ if [[ $1 =~ ^(y|yes)$ ]]
+ then
+ mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
+ create_db
+ echo "Recreated the database ($DB_NAME)."
+ else
+ echo "Leaving the existing database ($DB_NAME) in place."
+ fi
+ shopt -u nocasematch
+}
- # parse DB_HOST for port or socket references
- local PARTS=(${DB_HOST//\:/ })
- local DB_HOSTNAME=${PARTS[0]};
- local DB_SOCK_OR_PORT=${PARTS[1]};
- local EXTRA=""
-
- if ! [ -z $DB_HOSTNAME ] ; then
- if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
- EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
- elif ! [ -z $DB_SOCK_OR_PORT ] ; then
- EXTRA=" --socket=$DB_SOCK_OR_PORT"
- elif ! [ -z $DB_HOSTNAME ] ; then
- EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
- fi
- fi
+create_db() {
+ mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
+}
- if [ ${CREATE_DB_IF_EXISTS} = "true" ]; then
- mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA || echo "Database already exists."
- else
- mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
- fi
+install_db() {
+
+ if [ ${SKIP_DB_CREATE} = "true" ]; then
+ return 0
+ fi
+
+ # parse DB_HOST for port or socket references
+ local PARTS=(${DB_HOST//\:/ })
+ local DB_HOSTNAME=${PARTS[0]};
+ local DB_SOCK_OR_PORT=${PARTS[1]};
+ local EXTRA=""
+
+ if ! [ -z $DB_HOSTNAME ] ; then
+ if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
+ EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
+ elif ! [ -z $DB_SOCK_OR_PORT ] ; then
+ EXTRA=" --socket=$DB_SOCK_OR_PORT"
+ elif ! [ -z $DB_HOSTNAME ] ; then
+ EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
+ fi
+ fi
+
+ # create database
+ if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]
+ then
+ echo "Reinstalling will delete the existing test database ($DB_NAME)"
+ read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
+ recreate_db $DELETE_EXISTING_DB
+ else
+ create_db
+ fi
}
install_wp
install_test_suite
install_db
-
-echo "Done!"
From 7213999d074f669ac6a74a7dc2b2466496391eb7 Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Thu, 9 Jan 2025 16:21:04 -0300
Subject: [PATCH 04/23] Initial commit of php unit tests
---
tests/phpunit/REST/TestSearchTemplates.php | 67 ++++++++++
tests/phpunit/feature/TestSearchTemplates.php | 116 ++++++++++++++++++
2 files changed, 183 insertions(+)
create mode 100644 tests/phpunit/REST/TestSearchTemplates.php
create mode 100644 tests/phpunit/feature/TestSearchTemplates.php
diff --git a/tests/phpunit/REST/TestSearchTemplates.php b/tests/phpunit/REST/TestSearchTemplates.php
new file mode 100644
index 0000000..0b7a56c
--- /dev/null
+++ b/tests/phpunit/REST/TestSearchTemplates.php
@@ -0,0 +1,67 @@
+controller = new SearchTemplates();
+ }
+
+ /**
+ * Test the `get_search_templates` method
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_templates() {
+ $this->markTestIncomplete();
+ }
+ /**
+ * Test the `get_search_template` method
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_template() {
+ $this->markTestIncomplete();
+ }
+ /**
+ * Test the `update_search_template` method
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_update_search_template() {
+ $this->markTestIncomplete();
+ }
+ /**
+ * Test the `delete_search_template` method
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_delete_search_template() {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/tests/phpunit/feature/TestSearchTemplates.php b/tests/phpunit/feature/TestSearchTemplates.php
new file mode 100644
index 0000000..8373a74
--- /dev/null
+++ b/tests/phpunit/feature/TestSearchTemplates.php
@@ -0,0 +1,116 @@
+register_feature( $instance );
+ }
+
+ /**
+ * Get the Search Templates feature instance
+ *
+ * @return SearchTemplates
+ */
+ protected function get_feature() {
+ return \ElasticPress\Features::factory()->get_registered_feature( 'search_templates' );
+ }
+
+ /**
+ * Test the `requirements_status` method
+ *
+ * @group search-templates
+ */
+ public function test_requirements_status() {
+ // Should return 2 when not ep.io
+ $status = $this->get_feature()->requirements_status();
+
+ $this->assertEquals( 2, $status->code );
+
+ // Should return 1 if ep.io
+ $ep_host = function () {
+ return 'elasticpress.io/random-string';
+ };
+ add_filter( 'ep_host', $ep_host );
+ $status = $this->get_feature()->requirements_status();
+
+ $this->assertEquals( 1, $status->code );
+ }
+
+ /**
+ * Test the `is_search_templates_page` method
+ *
+ * @group search-templates
+ */
+ public function test_is_search_templates_page() {
+ set_current_screen();
+ $this->assertFalse( $this->get_feature()->is_search_templates_page() );
+
+ set_current_screen( 'elasticpress_page_elasticpress-search-templates' );
+ $this->assertTrue( $this->get_feature()->is_search_templates_page() );
+ }
+
+ /**
+ * Test the `setup_endpoint` method
+ *
+ * @group search-templates
+ */
+ public function test_setup_endpoint() {
+ $wp_rest_server = rest_get_server();
+
+ $routes = $wp_rest_server->get_routes( 'elasticpress-labs/v1' );
+ $this->assertEmpty( $routes );
+
+ $this->get_feature()->setup_endpoint();
+
+ $routes = $wp_rest_server->get_routes( 'elasticpress-labs/v1' );
+ $this->assertSame(
+ [
+ '/elasticpress-labs/v1',
+ '/elasticpress-labs/v1/search-templates',
+ '/elasticpress-labs/v1/search-templates/(?P[\w-]+)',
+ ],
+ array_keys( $routes )
+ );
+ }
+
+ /**
+ * Test the `set_settings_schema` method
+ *
+ * @group search-templates
+ */
+ public function test_set_settings_schema() {
+ $expected = [
+ [
+ 'default' => false,
+ 'key' => 'active',
+ 'label' => 'Enable',
+ 'requires_feature' => false,
+ 'requires_sync' => false,
+ 'type' => 'toggle',
+ ],
+ [
+ 'key' => 'additional_links',
+ 'label' => 'Manage search templates ',
+ 'type' => 'markup',
+ ],
+ ];
+
+ $this->assertSame( $expected, $this->get_feature()->get_settings_schema() );
+ }
+}
From 8407ba0ec3f599e90bafcba279930a00c9d6dbdb Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Fri, 10 Jan 2025 09:26:45 -0300
Subject: [PATCH 05/23] Adjust style handler
---
includes/classes/Feature/SearchTemplates.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
index 53cf5dc..cc28be0 100644
--- a/includes/classes/Feature/SearchTemplates.php
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -135,7 +135,7 @@ public function scripts() {
wp_enqueue_style( 'wp-edit-post' );
wp_enqueue_style(
- 'ep_synonyms_scripts',
+ 'ep_search_templates_scripts',
ELASTICPRESS_LABS_URL . 'dist/css/search-templates-script.css',
[],
LabsUtils\get_asset_info( 'search-templates-script', 'version' ),
From 99da0810cc4f4315e88992f3eb99a3288a33293d Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Fri, 10 Jan 2025 12:27:11 -0300
Subject: [PATCH 06/23] Unit tests
---
includes/classes/REST/SearchTemplates.php | 14 +-
tests/phpunit/REST/TestSearchTemplates.php | 278 ++++++++++++++++++++-
2 files changed, 283 insertions(+), 9 deletions(-)
diff --git a/includes/classes/REST/SearchTemplates.php b/includes/classes/REST/SearchTemplates.php
index 0aad442..76a86d5 100644
--- a/includes/classes/REST/SearchTemplates.php
+++ b/includes/classes/REST/SearchTemplates.php
@@ -156,11 +156,15 @@ public function update_search_template( \WP_REST_Request $request ) {
return $response;
}
+ $status_code = wp_remote_retrieve_response_code( $response );
if ( 201 !== wp_remote_retrieve_response_code( $response ) ) {
return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
}
- return json_decode( wp_remote_retrieve_body( $response ) );
+ $response_data = json_decode( wp_remote_retrieve_body( $response ) );
+ $rest_response = rest_ensure_response( $response_data );
+ $rest_response->set_status( $status_code );
+ return $rest_response;
}
/**
@@ -182,10 +186,14 @@ public function delete_search_template( \WP_REST_Request $request ) {
return $response;
}
- if ( 204 !== wp_remote_retrieve_response_code( $response ) ) {
+ $status_code = wp_remote_retrieve_response_code( $response );
+ if ( 204 !== $status_code ) {
return new \WP_Error( 'invalid_response', wp_remote_retrieve_response_message( $response ) );
}
- return wp_remote_retrieve_body( $response );
+ $response_data = wp_remote_retrieve_body( $response );
+ $rest_response = rest_ensure_response( $response_data );
+ $rest_response->set_status( $status_code );
+ return $rest_response;
}
}
diff --git a/tests/phpunit/REST/TestSearchTemplates.php b/tests/phpunit/REST/TestSearchTemplates.php
index 0b7a56c..91994a0 100644
--- a/tests/phpunit/REST/TestSearchTemplates.php
+++ b/tests/phpunit/REST/TestSearchTemplates.php
@@ -13,7 +13,7 @@
/**
* SearchTemplates test class
*/
-class TestSearchTemplates extends \WP_UnitTestCase {
+class TestSearchTemplates extends \ElasticPressLabsTest\BaseTestCase {
/**
* Controller instance
*
@@ -25,18 +25,151 @@ class TestSearchTemplates extends \WP_UnitTestCase {
* Setup each test.
*/
public function set_up() {
+ parent::set_up();
+
$this->controller = new SearchTemplates();
+ add_filter( 'ep_intercept_remote_request', '__return_true' );
+ }
+
+ /**
+ * Make sure access is restricted to admin users
+ *
+ * @param string $method HTTP method.
+ * @param int $expected HTTP expected status code
+ * @param string $path Endpoint path
+ * @group rest
+ * @group search-templates
+ * @dataProvider data_provider_endpoints_access
+ */
+ public function test_endpoints_access( $method, $expected, $path ) {
+ global $wp_rest_server;
+
+ \ElasticPress\Features::factory()->activate_feature( 'search_templates' );
+ \ElasticPress\Features::factory()->setup_features();
+
+ $return_http_code = function () use ( $expected ) {
+ return [
+ 'response' => [
+ 'code' => $expected,
+ 'message' => 'Testing message',
+ ],
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code );
+
+ $rest_request = new \WP_REST_Request( $method, $path );
+ $response = rest_do_request( $rest_request );
+ $this->assertSame( 401, $response->get_status(), "{$method}::{$path}" );
+
+ $admin_id = $this->ep_factory->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $admin_id );
+
+ $rest_request = new \WP_REST_Request( $method, $path );
+ $response = rest_do_request( $rest_request );
+ $this->assertSame( $expected, $response->get_status(), "{$method}::{$path}" );
+
+ // Reset the endpoints, so it does not interfere with other tests
+ $wp_rest_server = null;
+ }
+
+ /**
+ * Data provider for the test_endpoints_access method
+ *
+ * @return array
+ */
+ public function data_provider_endpoints_access() {
+ return [
+ [
+ 'method' => 'GET',
+ 'expected' => 200,
+ 'path' => '/elasticpress-labs/v1/search-templates',
+ ],
+ [
+ 'method' => 'GET',
+ 'expected' => 200,
+ 'path' => '/elasticpress-labs/v1/search-templates/template1',
+ ],
+ [
+ 'method' => 'PUT',
+ 'expected' => 201,
+ 'path' => '/elasticpress-labs/v1/search-templates/template1',
+ ],
+ [
+ 'method' => 'DELETE',
+ 'expected' => 204,
+ 'path' => '/elasticpress-labs/v1/search-templates/template1',
+ ],
+ ];
+ }
+
+ /**
+ * Test the `get_search_templates` method with a generic error
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_templates_generic_error() {
+ $generic_wp_error = $this->send_generic_error();
+ $this->assertSame( $generic_wp_error, $this->controller->get_search_templates() );
+ }
+
+ /**
+ * Test the `get_search_templates` method with an invalid response code
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_templates_invalid_status_code() {
+ $this->send_invalid_http_status_code();
+
+ $error = $this->controller->get_search_templates();
+ $this->assertEquals( 'invalid_response', $error->get_error_code() );
+ $this->assertEquals( 'Testing message', $error->get_error_message() );
}
/**
- * Test the `get_search_templates` method
+ * Test the `get_search_templates` method with a proper response
*
* @group rest
* @group search-templates
*/
public function test_get_search_templates() {
- $this->markTestIncomplete();
+ $return_http_code = function () {
+ return [
+ 'response' => [ 'code' => 200 ],
+ 'body' => '{"exampleorg-post-1":["template1","template2"], "exampleorg-post-2":["template3","template4"]}',
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code );
+
+ $this->assertSame( [ 'template1', 'template2' ], $this->controller->get_search_templates() );
}
+
+ /**
+ * Test the `get_search_template` method with a generic error
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_template_generic_error() {
+ $generic_wp_error = $this->send_generic_error();
+ $this->assertSame( $generic_wp_error, $this->controller->get_search_template( new \WP_REST_Request() ) );
+ }
+
+ /**
+ * Test the `get_search_template` method with an invalid response code
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_get_search_template_invalid_status_code() {
+ $this->send_invalid_http_status_code();
+
+ $error = $this->controller->get_search_template( new \WP_REST_Request() );
+ $this->assertEquals( 'invalid_response', $error->get_error_code() );
+ $this->assertEquals( 'Testing message', $error->get_error_message() );
+ }
+
/**
* Test the `get_search_template` method
*
@@ -44,8 +177,50 @@ public function test_get_search_templates() {
* @group search-templates
*/
public function test_get_search_template() {
- $this->markTestIncomplete();
+ $request = new \WP_REST_Request( 'GET', '/elasticpress-labs/v1/search-templates/template1' );
+ $request->set_param( 'template_name', 'template1' );
+
+ $return_http_code = function ( $response, $query ) {
+ $parts = wp_parse_url( $query['url'] );
+ parse_str( $parts['query'], $query );
+
+ $this->assertSame( 'template1', $query['template_name'] );
+
+ return [
+ 'response' => [ 'code' => 200 ],
+ 'body' => '{"a": "b"}',
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code, 10, 2 );
+
+ $this->assertEquals( (object) [ 'a' => 'b' ], $this->controller->get_search_template( $request ) );
+ }
+
+ /**
+ * Test the `update_search_template` method with a generic error
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_update_search_template_generic_error() {
+ $generic_wp_error = $this->send_generic_error();
+ $this->assertSame( $generic_wp_error, $this->controller->update_search_template( new \WP_REST_Request() ) );
}
+
+ /**
+ * Test the `update_search_template` method with an invalid response code
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_update_search_template_invalid_status_code() {
+ $this->send_invalid_http_status_code();
+
+ $error = $this->controller->update_search_template( new \WP_REST_Request() );
+ $this->assertEquals( 'invalid_response', $error->get_error_code() );
+ $this->assertEquals( 'Testing message', $error->get_error_message() );
+ }
+
/**
* Test the `update_search_template` method
*
@@ -53,8 +228,50 @@ public function test_get_search_template() {
* @group search-templates
*/
public function test_update_search_template() {
- $this->markTestIncomplete();
+ $request = new \WP_REST_Request( 'PUT', '/elasticpress-labs/v1/search-templates/template1' );
+ $request->set_param( 'template_name', 'template1' );
+
+ $return_http_code = function ( $response, $query ) {
+ $parts = wp_parse_url( $query['url'] );
+ parse_str( $parts['query'], $query );
+
+ $this->assertSame( 'template1', $query['template_name'] );
+
+ return [
+ 'response' => [ 'code' => 201 ],
+ 'body' => '{"a": "b"}',
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code, 10, 2 );
+
+ $this->assertEquals( (object) [ 'a' => 'b' ], $this->controller->update_search_template( $request )->get_data() );
}
+
+ /**
+ * Test the `delete_search_template` method with a generic error
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_delete_search_template_generic_error() {
+ $generic_wp_error = $this->send_generic_error();
+ $this->assertSame( $generic_wp_error, $this->controller->delete_search_template( new \WP_REST_Request() ) );
+ }
+
+ /**
+ * Test the `delete_search_template` method with an invalid response code
+ *
+ * @group rest
+ * @group search-templates
+ */
+ public function test_delete_search_template_invalid_status_code() {
+ $this->send_invalid_http_status_code();
+
+ $error = $this->controller->delete_search_template( new \WP_REST_Request() );
+ $this->assertEquals( 'invalid_response', $error->get_error_code() );
+ $this->assertEquals( 'Testing message', $error->get_error_message() );
+ }
+
/**
* Test the `delete_search_template` method
*
@@ -62,6 +279,55 @@ public function test_update_search_template() {
* @group search-templates
*/
public function test_delete_search_template() {
- $this->markTestIncomplete();
+ $request = new \WP_REST_Request( 'DELETE', '/elasticpress-labs/v1/search-templates/template1' );
+ $request->set_param( 'template_name', 'template1' );
+
+ $return_http_code = function ( $response, $query ) {
+ $parts = wp_parse_url( $query['url'] );
+ parse_str( $parts['query'], $query );
+
+ $this->assertSame( 'template1', $query['template_name'] );
+
+ return [
+ 'response' => [ 'code' => 204 ],
+ 'body' => '',
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code, 10, 2 );
+
+ $this->assertEquals( '', $this->controller->delete_search_template( $request )->get_data() );
+ }
+
+ /**
+ * Send a generic WP Error
+ *
+ * @return \WP_Error
+ */
+ protected function send_generic_error() {
+ $generic_wp_error = new \WP_Error( '0', 'Generic error' );
+
+ $return_callback = function () use ( $generic_wp_error ) {
+ return $generic_wp_error;
+ };
+ add_filter( 'ep_do_intercept_request', $return_callback );
+
+ return $generic_wp_error;
+ }
+
+ /**
+ * Send a 500 HTTP response
+ *
+ * @return void
+ */
+ protected function send_invalid_http_status_code() {
+ $return_http_code = function () {
+ return [
+ 'response' => [
+ 'code' => 500,
+ 'message' => 'Testing message',
+ ],
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code );
}
}
From 041d0d61f2e00f6e642d60e7c117c36fd4d2af5b Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 09:03:42 -0300
Subject: [PATCH 07/23] Ignore .wp-env.json in the final package
---
.gitattributes | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitattributes b/.gitattributes
index 8fb31de..5578f7a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -17,6 +17,7 @@
/.nvmrc export-ignore
/.stylelintignore export-ignore
/.stylelintrc export-ignore
+/.wp-env.json export-ignore
/babel.config.js export-ignore
/CHANGELOG.md export-ignore
/CODE_OF_CONDUCT.md export-ignore
From f8964e7cb274941de885aad6c3818a61409dc5af Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 13:03:47 -0300
Subject: [PATCH 08/23] e2e tests
---
.../components/new-template-row.js | 19 ++-
.../components/template-row.js | 20 ++-
assets/js/search-templates/provider.js | 16 +-
.../features/search-templates.cy.js | 157 ++++++++++++++++++
tests/cypress/support/global-hooks.js | 28 ++++
tests/cypress/support/index.js | 1 +
6 files changed, 222 insertions(+), 19 deletions(-)
create mode 100644 tests/cypress/integration/features/search-templates.cy.js
create mode 100644 tests/cypress/support/global-hooks.js
diff --git a/assets/js/search-templates/components/new-template-row.js b/assets/js/search-templates/components/new-template-row.js
index 169ba1f..06852d2 100644
--- a/assets/js/search-templates/components/new-template-row.js
+++ b/assets/js/search-templates/components/new-template-row.js
@@ -9,6 +9,7 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies.
*/
import { useSearchTemplateDispatch } from '../provider';
+import { useSettingsScreen } from '../../settings-screen';
import TemplateField from './template-field';
/**
@@ -21,11 +22,23 @@ export default () => {
const [template, setTemplate] = useState('');
const { saveTemplate } = useSearchTemplateDispatch();
+ const { createNotice } = useSettingsScreen();
const onAddNewTemplate = () => {
- saveTemplate(name, template);
- setName('');
- setTemplate('');
+ saveTemplate(name, template)
+ .then(() => {
+ setName('');
+ setTemplate('');
+ createNotice('success', __('Template saved.', 'elasticpress-labs'));
+ })
+ .catch((error) => {
+ createNotice(
+ 'error',
+ __('Could not save the template. Please try again.', 'elasticpress-labs'),
+ );
+ // eslint-disable-next-line no-console
+ console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
+ });
};
return (
diff --git a/assets/js/search-templates/components/template-row.js b/assets/js/search-templates/components/template-row.js
index b8b0a1b..91aeef5 100644
--- a/assets/js/search-templates/components/template-row.js
+++ b/assets/js/search-templates/components/template-row.js
@@ -90,14 +90,18 @@ export default ({ templateName }) => {
};
const onDeleteTemplate = () => {
- deleteTemplate(template).catch((error) => {
- createNotice(
- 'error',
- __('Could not delete the template. Please try again.', 'elasticpress-labs'),
- );
- // eslint-disable-next-line no-console
- console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
- });
+ deleteTemplate(templateName)
+ .then(() => {
+ createNotice('success', __('Template deleted.', 'elasticpress-labs'));
+ })
+ .catch((error) => {
+ createNotice(
+ 'error',
+ __('Could not delete the template. Please try again.', 'elasticpress-labs'),
+ );
+ // eslint-disable-next-line no-console
+ console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
+ });
};
return (
diff --git a/assets/js/search-templates/provider.js b/assets/js/search-templates/provider.js
index d9c9c95..09475bb 100644
--- a/assets/js/search-templates/provider.js
+++ b/assets/js/search-templates/provider.js
@@ -45,23 +45,23 @@ export const SearchTemplatesProvider = ({ children }) => {
});
const loadTemplate = async (template) => {
- const response = await apiFetch({
+ return apiFetch({
path: `${restApiEndpoint}/${template}`,
+ }).then((response) => {
+ dispatch({ type: 'SET_TEMPLATE', templateName: template, template: response });
+ return response;
});
-
- dispatch({ type: 'SET_TEMPLATE', templateName: template, template: response });
- return response;
};
const saveTemplate = async (name, template) => {
- const response = apiFetch({
+ return apiFetch({
path: `${restApiEndpoint}/${name}`,
method: 'PUT',
body: template,
+ }).then((response) => {
+ dispatch({ type: 'SET_TEMPLATE', templateName: name, template: response });
+ return response;
});
-
- dispatch({ type: 'SET_TEMPLATE', templateName: name, template: response });
- return response;
};
const deleteTemplate = (name) => {
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
new file mode 100644
index 0000000..9e798bd
--- /dev/null
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -0,0 +1,157 @@
+/* global isEpIo */
+
+describe('Search Templates Feature', () => {
+ const enableFeature = () => {
+ cy.visitAdminPage('admin.php?page=elasticpress');
+ cy.intercept('/wp-json/elasticpress/v1/features*').as('apiRequest');
+
+ cy.contains('button', 'Search Templates').click();
+ cy.contains('label', 'Enable')
+ .closest('.components-base-control__field')
+ .find('.components-form-toggle')
+ .as('toggle');
+
+ cy.get('@toggle').then(($el) => {
+ if ($el.hasClass('is-checked')) {
+ return;
+ }
+ cy.get('@toggle').click();
+ cy.contains('button', 'Save changes').click();
+
+ cy.wait('@apiRequest');
+ });
+ };
+
+ const deleteAllTemplates = () => {
+ cy.request('/wp-admin/admin-ajax.php?action=rest-nonce').then((response) => {
+ const nonce = response.body;
+ cy.request({
+ url: '/wp-json/elasticpress-labs/v1/search-templates',
+ headers: { 'x-wp-nonce': nonce },
+ }).then((response) => {
+ response.body.forEach((template) => {
+ cy.request({
+ method: 'DELETE',
+ url: `/wp-json/elasticpress-labs/v1/search-templates/${template}`,
+ headers: { 'x-wp-nonce': nonce },
+ });
+ });
+ });
+ });
+ };
+
+ /**
+ * Test that the feature cannot be activated when not in ElasticPress.io.
+ */
+ it("Can't activate the feature if not in ElasticPress.io", () => {
+ if (isEpIo) {
+ return;
+ }
+
+ cy.login();
+ cy.visitAdminPage('admin.php?page=elasticpress');
+
+ cy.contains('button', 'Search Templates').click();
+ cy.contains('.components-notice', 'You need an ElasticPress.io account').should('exist');
+ cy.get('.components-form-toggle__input').should('be.disabled');
+ });
+
+ it.only('Can manage search templates', () => {
+ if (!isEpIo) {
+ return;
+ }
+
+ cy.login();
+ enableFeature();
+
+ deleteAllTemplates();
+
+ /**
+ * Can go to the Search Templates page through the features section
+ */
+ cy.visitAdminPage('admin.php?page=elasticpress');
+ cy.contains('button', 'Search Templates').click();
+ cy.contains('a', 'Manage search templates').click();
+
+ cy.url().should('include', 'elasticpress-search-templates');
+
+ /**
+ * Can add a new template
+ */
+ cy.contains('.components-panel__body-title', 'Add New Template')
+ .closest('.components-panel__body')
+ .as('addNewTemplatePanel');
+
+ cy.get('@addNewTemplatePanel').get('input[type="text"]').type('new-template');
+ cy.get('@addNewTemplatePanel')
+ .get('textarea')
+ .type('{"a": "b"},', { parseSpecialCharSequences: false });
+ cy.contains('.components-notice', 'This does not seem to be a valid JSON object.').should(
+ 'exist',
+ );
+
+ cy.get('@addNewTemplatePanel').get('textarea').as('addNewTemplateTextarea');
+ cy.get('@addNewTemplateTextarea').clear();
+ cy.get('@addNewTemplateTextarea').type('{"a": "b"}', { parseSpecialCharSequences: false });
+ cy.contains('.components-notice', 'This does not seem to be a valid JSON object.').should(
+ 'not.exist',
+ );
+
+ cy.get('@addNewTemplatePanel').contains('button', 'Save Template').click();
+ cy.contains('Template saved.').should('exist');
+
+ cy.contains('.components-panel__body-title', 'new-template')
+ .closest('.components-panel__body')
+ .as('NewTemplatePanel');
+ cy.get('@NewTemplatePanel').should('exist');
+
+ cy.get('@NewTemplatePanel').click();
+
+ cy.get('@NewTemplatePanel')
+ .find('input[type="text"]')
+ .should('have.value', 'new-template')
+ .should('be.disabled');
+ cy.get('@NewTemplatePanel')
+ .find('textarea')
+ .invoke('val')
+ .then((val) => expect(JSON.stringify(JSON.parse(val))).to.equal('{"a":"b"}'));
+
+ /**
+ * Can edit a template
+ */
+ cy.get('@NewTemplatePanel').find('textarea').as('newTemplateTextarea');
+ cy.get('@newTemplateTextarea').clear();
+ cy.get('@newTemplateTextarea').type('{"a": "c"}', { parseSpecialCharSequences: false });
+ cy.get('@NewTemplatePanel').contains('button', 'Save changes').click();
+ cy.contains('Template saved.').should('exist');
+
+ cy.visitAdminPage('admin.php?page=elasticpress-search-templates');
+ cy.contains('.components-panel__body-title', 'new-template')
+ .closest('.components-panel__body')
+ .as('NewTemplatePanel');
+ cy.get('@NewTemplatePanel').should('exist');
+
+ cy.intercept('/wp-json/elasticpress-labs/v1/search-templates/new-template*').as(
+ 'loadTemplateRequest',
+ );
+ cy.get('@NewTemplatePanel').click();
+ cy.wait('@loadTemplateRequest');
+
+ cy.get('@NewTemplatePanel')
+ .find('textarea')
+ .invoke('val')
+ .then((val) => expect(JSON.stringify(JSON.parse(val))).to.equal('{"a":"c"}'));
+
+ /**
+ * Can delete a template
+ */
+ cy.visitAdminPage('admin.php?page=elasticpress-search-templates');
+ cy.contains('.components-panel__body-title', 'new-template')
+ .closest('.components-panel__body')
+ .as('NewTemplatePanel');
+ cy.get('@NewTemplatePanel').click();
+
+ cy.get('@NewTemplatePanel').contains('button', 'Delete template').click();
+ cy.contains('Template deleted.').should('exist');
+ });
+});
diff --git a/tests/cypress/support/global-hooks.js b/tests/cypress/support/global-hooks.js
new file mode 100644
index 0000000..191ebc1
--- /dev/null
+++ b/tests/cypress/support/global-hooks.js
@@ -0,0 +1,28 @@
+window.indexNames = null;
+window.isEpIo = false;
+window.wpVersion = '';
+
+before(() => {
+ cy.wpCliEval(
+ `
+ // Clear any stuck sync process.
+ \\ElasticPress\\IndexHelper::factory()->clear_index_meta();
+
+ $is_epio = (int) \\ElasticPress\\Utils\\is_epio();
+
+ $index_names = \\ElasticPress\\Elasticsearch::factory()->get_index_names( 'active' );
+ echo wp_json_encode(
+ [
+ 'indexNames' => $index_names,
+ 'isEpIo' => $is_epio,
+ 'wpVersion' => get_bloginfo( 'version' ),
+ ]
+ );
+ `,
+ ).then((wpCliResponse) => {
+ const wpCliRespObj = JSON.parse(wpCliResponse.stdout);
+ window.indexNames = wpCliRespObj.indexNames;
+ window.isEpIo = wpCliRespObj.isEpIo === 1;
+ window.wpVersion = wpCliRespObj.wpVersion;
+ });
+});
diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js
index 921f059..81a1285 100644
--- a/tests/cypress/support/index.js
+++ b/tests/cypress/support/index.js
@@ -15,6 +15,7 @@
import '@10up/cypress-wp-utils';
import './commands';
+import './global-hooks';
/**
* Ignore ResizeObserver error.
From e7bace5ac144c942dce6084cdbc1ea849be37acc Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 13:50:49 -0300
Subject: [PATCH 09/23] Overwrite wpCliEval as the path is always
elasticpress-labs
---
tests/cypress/.gitignore | 2 ++
tests/cypress/support/commands.js | 16 ++++++++++++++++
2 files changed, 18 insertions(+)
create mode 100644 tests/cypress/.gitignore
diff --git a/tests/cypress/.gitignore b/tests/cypress/.gitignore
new file mode 100644
index 0000000..5915326
--- /dev/null
+++ b/tests/cypress/.gitignore
@@ -0,0 +1,2 @@
+logs
+videos
diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js
index 9a3535b..16849f5 100644
--- a/tests/cypress/support/commands.js
+++ b/tests/cypress/support/commands.js
@@ -44,3 +44,19 @@ Cypress.Commands.add('clearThenType', { prevSubject: true }, (subject, text, for
cy.wrap(subject).clear();
cy.wrap(subject).type(text, { force });
});
+
+Cypress.Commands.add('wpCliEval', (command) => {
+ const fileName = (Math.random() + 1).toString(36).substring(7);
+
+ // this will be written "local" plugin directory
+ const escapedCommand = command.replace(/^<\?php /, '');
+ cy.writeFile(fileName, ` {
+ cy.exec(`rm ${fileName}`);
+ cy.wrap(result);
+ });
+});
From 233662aa473a05a386253b428e18c9d96e0ccf5f Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 14:15:45 -0300
Subject: [PATCH 10/23] Setup permalinks structure
---
bin/setup-cypress-env.sh | 2 ++
tests/cypress/integration/features/search-templates.cy.js | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/bin/setup-cypress-env.sh b/bin/setup-cypress-env.sh
index ec8cdbd..a686256 100755
--- a/bin/setup-cypress-env.sh
+++ b/bin/setup-cypress-env.sh
@@ -56,6 +56,8 @@ fi
./bin/wp-env-cli tests-wordpress "wp --allow-root plugin activate elasticpress-labs"
+./bin/wp-env-cli tests-wordpress "wp --allow-root rewrite structure '/%postname%/'"
+
if [ -z $EP_HOST ]; then
# Determine what kind of env we're in
if [ "$(uname | tr '[:upper:]' '[:lower:]')" = "darwin" ]; then
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index 9e798bd..65348b7 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -56,7 +56,7 @@ describe('Search Templates Feature', () => {
cy.get('.components-form-toggle__input').should('be.disabled');
});
- it.only('Can manage search templates', () => {
+ it('Can manage search templates', () => {
if (!isEpIo) {
return;
}
From c55fee32872c020b600738204e206b6bb4ebc8ba Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 15:59:44 -0300
Subject: [PATCH 11/23] Test templates limit
---
.../components/new-template-row.js | 26 ++++++++--
includes/classes/REST/SearchTemplates.php | 7 +--
.../features/search-templates.cy.js | 49 +++++++++++++++++++
3 files changed, 74 insertions(+), 8 deletions(-)
diff --git a/assets/js/search-templates/components/new-template-row.js b/assets/js/search-templates/components/new-template-row.js
index 06852d2..fbea861 100644
--- a/assets/js/search-templates/components/new-template-row.js
+++ b/assets/js/search-templates/components/new-template-row.js
@@ -1,14 +1,14 @@
/**
* WordPress Dependencies.
*/
-import { Button, Flex, PanelBody, PanelRow, TextControl } from '@wordpress/components';
+import { Button, Flex, Notice, PanelBody, PanelRow, TextControl } from '@wordpress/components';
import { useState, WPElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies.
*/
-import { useSearchTemplateDispatch } from '../provider';
+import { useSearchTemplate, useSearchTemplateDispatch } from '../provider';
import { useSettingsScreen } from '../../settings-screen';
import TemplateField from './template-field';
@@ -20,7 +20,9 @@ import TemplateField from './template-field';
export default () => {
const [name, setName] = useState('');
const [template, setTemplate] = useState('');
+ const [disabled, setDisabled] = useState(false);
+ const { templates } = useSearchTemplate();
const { saveTemplate } = useSearchTemplateDispatch();
const { createNotice } = useSettingsScreen();
@@ -34,17 +36,31 @@ export default () => {
.catch((error) => {
createNotice(
'error',
- __('Could not save the template. Please try again.', 'elasticpress-labs'),
+ error.message ||
+ __('Could not save the template. Please try again.', 'elasticpress-labs'),
);
// eslint-disable-next-line no-console
console.error(__('ElasticPress Labs Error: ', 'elasticpress-labs'), error);
});
};
+ const onChangeName = (newName) => {
+ setName(newName);
+ setDisabled(Object.keys(templates).includes(newName));
+ };
+
return (
+ {name && disabled && (
+
+ {__(
+ 'This name is already in use. You can change the existing template instead.',
+ 'elasticpress-labs',
+ )}
+
+ )}
{
'elasticpress-labs',
)}
value={name}
- onChange={setName}
+ onChange={onChangeName}
/>
set_status( $status_code );
return $rest_response;
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index 65348b7..f52c00f 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -131,6 +131,12 @@ describe('Search Templates Feature', () => {
.as('NewTemplatePanel');
cy.get('@NewTemplatePanel').should('exist');
+ cy.contains('.components-panel__body-title', 'Add New Template')
+ .closest('.components-panel__body')
+ .as('addNewTemplatePanel');
+ cy.get('@addNewTemplatePanel').get('input[type="text"]').type('new-template');
+ cy.contains('.components-notice', 'This name is already in use.').should('not.exist');
+
cy.intercept('/wp-json/elasticpress-labs/v1/search-templates/new-template*').as(
'loadTemplateRequest',
);
@@ -154,4 +160,47 @@ describe('Search Templates Feature', () => {
cy.get('@NewTemplatePanel').contains('button', 'Delete template').click();
cy.contains('Template deleted.').should('exist');
});
+
+ it('Can see a message if above limits', () => {
+ if (!isEpIo) {
+ return;
+ }
+
+ cy.login();
+ enableFeature();
+ deleteAllTemplates();
+
+ cy.request('/wp-admin/admin-ajax.php?action=rest-nonce').then((response) => {
+ const nonce = response.body;
+ // The test account already has a template created under a different index prefix.
+ for (let index = 1; index <= 9; index++) {
+ cy.request({
+ method: 'PUT',
+ url: `/wp-json/elasticpress-labs/v1/search-templates/template-${index}`,
+ body: '{"a": "b"}',
+ headers: { 'x-wp-nonce': nonce },
+ });
+ // Give the server a small break between requests
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
+ cy.wait(200);
+ }
+ });
+
+ cy.visitAdminPage('admin.php?page=elasticpress-search-templates');
+ cy.contains('.components-panel__body-title', 'Add New Template')
+ .closest('.components-panel__body')
+ .as('addNewTemplatePanel');
+ cy.get('@addNewTemplatePanel').get('input[type="text"]').type('new-template');
+ cy.get('@addNewTemplatePanel')
+ .get('textarea')
+ .type('{"a": "b"}', { parseSpecialCharSequences: false });
+
+ cy.intercept('/wp-json/elasticpress-labs/v1/search-templates/new-template*').as(
+ 'loadTemplateRequest',
+ );
+ cy.get('@addNewTemplatePanel').contains('button', 'Save Template').click();
+ cy.wait('@loadTemplateRequest');
+
+ cy.contains('It seems you have reached the limit of search').should('exist');
+ });
});
From fdb73a36f5d94ee4bd8301c4c03812268613dd9c Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 16:48:46 -0300
Subject: [PATCH 12/23] Delete all templates in the account
---
.wp-env.json | 1 +
includes/classes/Feature/SearchTemplates.php | 23 +++++++++++++++
.../features/search-templates.cy.js | 27 ++++-------------
.../test-mu-plugins/delete-all-templates.php | 29 +++++++++++++++++++
tests/phpunit/REST/TestSearchTemplates.php | 2 +-
5 files changed, 59 insertions(+), 23 deletions(-)
create mode 100644 tests/cypress/wordpress-files/test-mu-plugins/delete-all-templates.php
diff --git a/.wp-env.json b/.wp-env.json
index d36a180..7b6644e 100644
--- a/.wp-env.json
+++ b/.wp-env.json
@@ -15,6 +15,7 @@
"mappings": {
"wp-content/plugins/elasticpress-labs": ".",
".htaccess": "./tests/cypress/wordpress-files/.htaccess",
+ "wp-content/mu-plugins/delete-all-templates.php": "./tests/cypress/wordpress-files/test-mu-plugins/delete-all-templates.php",
"wp-content/mu-plugins/unique-index-name.php": "./tests/cypress/wordpress-files/test-mu-plugins/unique-index-name.php"
}
}
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
index cc28be0..0741008 100644
--- a/includes/classes/Feature/SearchTemplates.php
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -183,6 +183,29 @@ public function admin_page() {
remote_request( 'api/v1/search/posts/templates' );
+ $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
+
+ if ( ! empty( $response_body ) ) {
+ foreach ( $response_body as $index_name => $templates ) {
+ foreach ( $templates as $template ) {
+ \ElasticPress\Elasticsearch::factory()->remote_request(
+ 'api/v1/search/posts/' . $index_name . '/template?template_name=' . $template,
+ [ 'method' => 'DELETE' ]
+ );
+ }
+ }
+ }
+ }
+
/**
* Set the `settings_schema` attribute
*/
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index f52c00f..4abed1c 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -22,24 +22,6 @@ describe('Search Templates Feature', () => {
});
};
- const deleteAllTemplates = () => {
- cy.request('/wp-admin/admin-ajax.php?action=rest-nonce').then((response) => {
- const nonce = response.body;
- cy.request({
- url: '/wp-json/elasticpress-labs/v1/search-templates',
- headers: { 'x-wp-nonce': nonce },
- }).then((response) => {
- response.body.forEach((template) => {
- cy.request({
- method: 'DELETE',
- url: `/wp-json/elasticpress-labs/v1/search-templates/${template}`,
- headers: { 'x-wp-nonce': nonce },
- });
- });
- });
- });
- };
-
/**
* Test that the feature cannot be activated when not in ElasticPress.io.
*/
@@ -64,7 +46,7 @@ describe('Search Templates Feature', () => {
cy.login();
enableFeature();
- deleteAllTemplates();
+ cy.wpCli('wp elasticpress-tests delete-all-search-templates');
/**
* Can go to the Search Templates page through the features section
@@ -135,7 +117,7 @@ describe('Search Templates Feature', () => {
.closest('.components-panel__body')
.as('addNewTemplatePanel');
cy.get('@addNewTemplatePanel').get('input[type="text"]').type('new-template');
- cy.contains('.components-notice', 'This name is already in use.').should('not.exist');
+ cy.contains('.components-notice', 'This name is already in use.').should('exist');
cy.intercept('/wp-json/elasticpress-labs/v1/search-templates/new-template*').as(
'loadTemplateRequest',
@@ -168,12 +150,13 @@ describe('Search Templates Feature', () => {
cy.login();
enableFeature();
- deleteAllTemplates();
+
+ cy.wpCli('wp elasticpress-tests delete-all-search-templates');
cy.request('/wp-admin/admin-ajax.php?action=rest-nonce').then((response) => {
const nonce = response.body;
// The test account already has a template created under a different index prefix.
- for (let index = 1; index <= 9; index++) {
+ for (let index = 1; index <= 10; index++) {
cy.request({
method: 'PUT',
url: `/wp-json/elasticpress-labs/v1/search-templates/template-${index}`,
diff --git a/tests/cypress/wordpress-files/test-mu-plugins/delete-all-templates.php b/tests/cypress/wordpress-files/test-mu-plugins/delete-all-templates.php
new file mode 100644
index 0000000..e0e2078
--- /dev/null
+++ b/tests/cypress/wordpress-files/test-mu-plugins/delete-all-templates.php
@@ -0,0 +1,29 @@
+get_registered_feature( 'search_templates' );
+ if ( $search_templates ) {
+ $search_templates->delete_all_search_templates();
+ WP_CLI::success( 'All templates deleted.' );
+ } else {
+ WP_CLI::error( 'Search templates feature not activated.' );
+ }
+}
+WP_CLI::add_command( 'elasticpress-tests delete-all-search-templates', 'ep_tests_delete_all_search_templates' );
diff --git a/tests/phpunit/REST/TestSearchTemplates.php b/tests/phpunit/REST/TestSearchTemplates.php
index 91994a0..e57d023 100644
--- a/tests/phpunit/REST/TestSearchTemplates.php
+++ b/tests/phpunit/REST/TestSearchTemplates.php
@@ -217,7 +217,7 @@ public function test_update_search_template_invalid_status_code() {
$this->send_invalid_http_status_code();
$error = $this->controller->update_search_template( new \WP_REST_Request() );
- $this->assertEquals( 'invalid_response', $error->get_error_code() );
+ $this->assertEquals( 500, $error->get_error_code() );
$this->assertEquals( 'Testing message', $error->get_error_message() );
}
From 06515f47f7b3f70f10758cb2e3281a58297114ce Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 17:14:09 -0300
Subject: [PATCH 13/23] Unit test for test_delete_all_search_templates
---
tests/phpunit/feature/TestSearchTemplates.php | 50 +++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/tests/phpunit/feature/TestSearchTemplates.php b/tests/phpunit/feature/TestSearchTemplates.php
index 8373a74..dd9517c 100644
--- a/tests/phpunit/feature/TestSearchTemplates.php
+++ b/tests/phpunit/feature/TestSearchTemplates.php
@@ -89,6 +89,56 @@ public function test_setup_endpoint() {
);
}
+ /**
+ * Test the `delete_all_search_templates` method
+ *
+ * @group search-templates
+ */
+ public function test_delete_all_search_templates() {
+ $return_http_code = function ( $response, $request ) {
+ static $calls = 0;
+
+ if ( 'GET' === $request['args']['method'] && str_ends_with( $request['url'], '/api/v1/search/posts/templates' ) ) {
+ return [
+ 'response' => [
+ 'code' => 200,
+ ],
+ 'body' => wp_json_encode(
+ [
+ 'index1' => [ 'template1', 'template2' ],
+ 'index2' => [ 'template3' ],
+ ]
+ ),
+ ];
+ }
+
+ $this->assertSame( 'DELETE', $request['args']['method'] );
+
+ $expected = [
+ [ 'index1', 'template1' ],
+ [ 'index1', 'template2' ],
+ [ 'index2', 'template3' ],
+ ];
+
+ $expected_index = $expected[ $calls ][0];
+ $expected_template = $expected[ $calls ][1];
+ $this->assertStringEndsWith( "api/v1/search/posts/{$expected_index}/template?template_name={$expected_template}", $request['url'] );
+
+ $calls++;
+
+ return [
+ 'response' => [
+ 'code' => 200,
+ 'message' => 'Testing message',
+ ],
+ ];
+ };
+ add_filter( 'ep_do_intercept_request', $return_http_code, 10, 2 );
+ add_filter( 'ep_intercept_remote_request', '__return_true' );
+
+ $this->get_feature()->delete_all_search_templates();
+ }
+
/**
* Test the `set_settings_schema` method
*
From 256f100c78acf8a618660ef823fdc60649783e5d Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 17:25:27 -0300
Subject: [PATCH 14/23] Fix unit tests
---
includes/classes/Feature/SearchTemplates.php | 30 +++++++++++--
includes/classes/REST/SearchTemplates.php | 45 +++++++++-----------
tests/phpunit/REST/TestSearchTemplates.php | 5 ++-
3 files changed, 51 insertions(+), 29 deletions(-)
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
index 0741008..acfb8e2 100644
--- a/includes/classes/Feature/SearchTemplates.php
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -164,7 +164,7 @@ public function scripts() {
* Setup REST endpoints
*/
public function setup_endpoint() {
- $controller = new \ElasticPressLabs\REST\SearchTemplates();
+ $controller = new \ElasticPressLabs\REST\SearchTemplates( $this );
$controller->register_routes();
}
@@ -191,14 +191,14 @@ public function admin_page() {
* @return void
*/
public function delete_all_search_templates() {
- $response = \ElasticPress\Elasticsearch::factory()->remote_request( 'api/v1/search/posts/templates' );
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request( $this->get_search_templates_endpoint() );
$response_body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $response_body ) ) {
foreach ( $response_body as $index_name => $templates ) {
foreach ( $templates as $template ) {
\ElasticPress\Elasticsearch::factory()->remote_request(
- 'api/v1/search/posts/' . $index_name . '/template?template_name=' . $template,
+ $this->get_search_template_endpoint( $index_name ) . '?template_name=' . $template,
[ 'method' => 'DELETE' ]
);
}
@@ -206,6 +206,30 @@ public function delete_all_search_templates() {
}
}
+ /**
+ * EP.io search templates endpoint.
+ *
+ * @return string
+ */
+ public function get_search_templates_endpoint(): string {
+ return 'api/v1/search/posts/templates';
+ }
+
+ /**
+ * EP.io (single) search template endpoint.
+ *
+ * @param null|string $index_name Index name.
+ * @return string
+ */
+ public function get_search_template_endpoint( $index_name = null ): string {
+ if ( ! $index_name ) {
+ $index_name = \ElasticPress\Indexables::factory()->get( 'post' )->get_index_name();
+ }
+
+ return "api/v1/search/posts/{$index_name}/template";
+ }
+
+
/**
* Set the `settings_schema` attribute
*/
diff --git a/includes/classes/REST/SearchTemplates.php b/includes/classes/REST/SearchTemplates.php
index 74afeac..8468a6b 100644
--- a/includes/classes/REST/SearchTemplates.php
+++ b/includes/classes/REST/SearchTemplates.php
@@ -9,11 +9,28 @@
namespace ElasticPressLabs\REST;
use ElasticPress\Utils;
+use ElasticPressLabs\Feature\SearchTemplates as SearchTemplatesFeature;
/**
* Search Templates API controller class.
*/
class SearchTemplates {
+ /**
+ * The SearchTemplatesFeature instance.
+ *
+ * @var SearchTemplatesFeature
+ */
+ protected $feature;
+
+ /**
+ * Class constructor
+ *
+ * @param SearchTemplatesFeature $feature The feature instance.
+ */
+ public function __construct( SearchTemplatesFeature $feature ) {
+ $this->feature = $feature;
+ }
+
/**
* Register routes.
*
@@ -70,33 +87,13 @@ public function check_permission() {
return current_user_can( $capability );
}
- /**
- * EP.io search templates endpoint.
- *
- * @return string
- */
- protected function get_search_templates_endpoint(): string {
- return 'api/v1/search/posts/templates';
- }
-
- /**
- * EP.io (single) search template endpoint.
- *
- * @return string
- */
- protected function get_search_template_endpoint(): string {
- $index_name = \ElasticPress\Indexables::factory()->get( 'post' )->get_index_name();
-
- return "api/v1/search/posts/{$index_name}/template";
- }
-
/**
* List search templates handler.
*
* @return array|\WP_Error
*/
public function get_search_templates() {
- $response = \ElasticPress\Elasticsearch::factory()->remote_request( $this->get_search_templates_endpoint() );
+ $response = \ElasticPress\Elasticsearch::factory()->remote_request( $this->feature->get_search_templates_endpoint() );
if ( is_wp_error( $response ) ) {
return $response;
@@ -119,7 +116,7 @@ public function get_search_templates() {
* @return object|\WP_Error
*/
public function get_search_template( \WP_REST_Request $request ) {
- $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $path = $this->feature->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
$response = \ElasticPress\Elasticsearch::factory()->remote_request( $path );
if ( is_wp_error( $response ) ) {
@@ -140,7 +137,7 @@ public function get_search_template( \WP_REST_Request $request ) {
* @return object|\WP_Error
*/
public function update_search_template( \WP_REST_Request $request ) {
- $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $path = $this->feature->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
$response = \ElasticPress\Elasticsearch::factory()->remote_request(
$path,
[
@@ -175,7 +172,7 @@ public function update_search_template( \WP_REST_Request $request ) {
* @return object|\WP_Error
*/
public function delete_search_template( \WP_REST_Request $request ) {
- $path = $this->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
+ $path = $this->feature->get_search_template_endpoint() . '?template_name=' . $request['template_name'];
$response = \ElasticPress\Elasticsearch::factory()->remote_request(
$path,
[
diff --git a/tests/phpunit/REST/TestSearchTemplates.php b/tests/phpunit/REST/TestSearchTemplates.php
index e57d023..9431183 100644
--- a/tests/phpunit/REST/TestSearchTemplates.php
+++ b/tests/phpunit/REST/TestSearchTemplates.php
@@ -27,7 +27,7 @@ class TestSearchTemplates extends \ElasticPressLabsTest\BaseTestCase {
public function set_up() {
parent::set_up();
- $this->controller = new SearchTemplates();
+ $this->controller = new SearchTemplates( \ElasticPress\Features::factory()->get_registered_feature( 'search_templates' ) );
add_filter( 'ep_intercept_remote_request', '__return_true' );
}
@@ -218,7 +218,7 @@ public function test_update_search_template_invalid_status_code() {
$error = $this->controller->update_search_template( new \WP_REST_Request() );
$this->assertEquals( 500, $error->get_error_code() );
- $this->assertEquals( 'Testing message', $error->get_error_message() );
+ $this->assertEquals( 'Testing body message', $error->get_error_message() );
}
/**
@@ -326,6 +326,7 @@ protected function send_invalid_http_status_code() {
'code' => 500,
'message' => 'Testing message',
],
+ 'body' => 'Testing body message',
];
};
add_filter( 'ep_do_intercept_request', $return_http_code );
From 6161fb2764acadcf81fa31def1fb58a8df6ad18a Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 17:28:23 -0300
Subject: [PATCH 15/23] php lint
---
tests/phpunit/REST/TestSearchTemplates.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/phpunit/REST/TestSearchTemplates.php b/tests/phpunit/REST/TestSearchTemplates.php
index 9431183..22d9c66 100644
--- a/tests/phpunit/REST/TestSearchTemplates.php
+++ b/tests/phpunit/REST/TestSearchTemplates.php
@@ -326,7 +326,7 @@ protected function send_invalid_http_status_code() {
'code' => 500,
'message' => 'Testing message',
],
- 'body' => 'Testing body message',
+ 'body' => 'Testing body message',
];
};
add_filter( 'ep_do_intercept_request', $return_http_code );
From 9f7ba84070720c325d3444c2fe9a80d5bf4c3927 Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Tue, 14 Jan 2025 17:42:28 -0300
Subject: [PATCH 16/23] php lint
---
tests/phpunit/feature/TestSearchTemplates.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/phpunit/feature/TestSearchTemplates.php b/tests/phpunit/feature/TestSearchTemplates.php
index dd9517c..533390f 100644
--- a/tests/phpunit/feature/TestSearchTemplates.php
+++ b/tests/phpunit/feature/TestSearchTemplates.php
@@ -124,7 +124,7 @@ public function test_delete_all_search_templates() {
$expected_template = $expected[ $calls ][1];
$this->assertStringEndsWith( "api/v1/search/posts/{$expected_index}/template?template_name={$expected_template}", $request['url'] );
- $calls++;
+ ++$calls;
return [
'response' => [
From 880739ed69993b7de5620e781cd92cc3dfed455b Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Wed, 15 Jan 2025 08:27:28 -0300
Subject: [PATCH 17/23] Remove empty space
---
.github/workflows/test.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3266191..f4180b2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -58,7 +58,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install subversion
-
+
- name: Setup WP Tests
run: |
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1
From 25764dd7e515b21d412b8da6e61e279e769e10aa Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Wed, 15 Jan 2025 08:28:30 -0300
Subject: [PATCH 18/23] Remove empty lines
---
bin/install-wp-tests.sh | 2 +-
tests/cypress/.gitignore | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh
index d2605fe..6341bdc 100644
--- a/bin/install-wp-tests.sh
+++ b/bin/install-wp-tests.sh
@@ -191,4 +191,4 @@ install_db() {
install_wp
install_test_suite
-install_db
+install_db
\ No newline at end of file
diff --git a/tests/cypress/.gitignore b/tests/cypress/.gitignore
index 5915326..7a21262 100644
--- a/tests/cypress/.gitignore
+++ b/tests/cypress/.gitignore
@@ -1,2 +1,2 @@
logs
-videos
+videos
\ No newline at end of file
From 342accee65afb172b6baebd4e2855da36973cb3c Mon Sep 17 00:00:00 2001
From: Felipe Elia
Date: Wed, 22 Jan 2025 08:29:42 -0300
Subject: [PATCH 19/23] Fix text domains
---
assets/js/search-templates/apps/search-templates-list.js | 5 ++++-
includes/classes/Feature/SearchTemplates.php | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/assets/js/search-templates/apps/search-templates-list.js b/assets/js/search-templates/apps/search-templates-list.js
index c1f901e..cb0dc6b 100644
--- a/assets/js/search-templates/apps/search-templates-list.js
+++ b/assets/js/search-templates/apps/search-templates-list.js
@@ -46,7 +46,10 @@ export default () => {
{createInterpolateElement(
- sprintf(__('Endpoint URL: %s
'), endpointExample),
+ sprintf(
+ __('Endpoint URL: %s
', 'elasticpress-labs'),
+ endpointExample,
+ ),
{ strong: , code:
},
)}
diff --git a/includes/classes/Feature/SearchTemplates.php b/includes/classes/Feature/SearchTemplates.php
index acfb8e2..8bf6ea4 100644
--- a/includes/classes/Feature/SearchTemplates.php
+++ b/includes/classes/Feature/SearchTemplates.php
@@ -47,7 +47,7 @@ public function __construct() {
$this->search_api_docs_url
) . '' .
'' . __( 'Please note that all the API fields are still available for custom search templates. Your templates do not to differ in post types, offset, pagination arguments, or even filters, as for those you can still use query parameters. The templates can be used for searching in different fields or applying different scores, for instance.', 'elasticpress-labs' ) . '
' .
- '' . __( 'Requires an ElasticPress.io plan to function.', 'elasticpress' ) . '
';
+ '' . __( 'Requires an ElasticPress.io plan to function.', 'elasticpress-labs' ) . '
';
parent::__construct();
}
From 6fdee9f1571d5a06da824fe9710cf81faee7fc56 Mon Sep 17 00:00:00 2001
From: Burhan Nasir
Date: Tue, 28 Jan 2025 13:48:39 +0500
Subject: [PATCH 20/23] Run delete template CLI before running the tests
---
tests/cypress/integration/features/search-templates.cy.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index 4abed1c..050fa2f 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -1,6 +1,10 @@
/* global isEpIo */
describe('Search Templates Feature', () => {
+ before(() => {
+ cy.wpCli('wp elasticpress-tests delete-all-search-templates');
+ });
+
const enableFeature = () => {
cy.visitAdminPage('admin.php?page=elasticpress');
cy.intercept('/wp-json/elasticpress/v1/features*').as('apiRequest');
@@ -46,8 +50,6 @@ describe('Search Templates Feature', () => {
cy.login();
enableFeature();
- cy.wpCli('wp elasticpress-tests delete-all-search-templates');
-
/**
* Can go to the Search Templates page through the features section
*/
From 943161eb152dd534c9fdd5401b53d3b694ea67d7 Mon Sep 17 00:00:00 2001
From: Burhan Nasir
Date: Tue, 28 Jan 2025 13:55:32 +0500
Subject: [PATCH 21/23] Cleanup for search template
---
.../cypress/integration/features/search-templates.cy.js | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index 050fa2f..4190015 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -1,10 +1,6 @@
/* global isEpIo */
describe('Search Templates Feature', () => {
- before(() => {
- cy.wpCli('wp elasticpress-tests delete-all-search-templates');
- });
-
const enableFeature = () => {
cy.visitAdminPage('admin.php?page=elasticpress');
cy.intercept('/wp-json/elasticpress/v1/features*').as('apiRequest');
@@ -50,6 +46,8 @@ describe('Search Templates Feature', () => {
cy.login();
enableFeature();
+ cy.wpCli('wp elasticpress-tests delete-all-search-templates');
+
/**
* Can go to the Search Templates page through the features section
*/
@@ -187,5 +185,8 @@ describe('Search Templates Feature', () => {
cy.wait('@loadTemplateRequest');
cy.contains('It seems you have reached the limit of search').should('exist');
+
+ // clean up
+ cy.wpCli('wp elasticpress-tests delete-all-search-templates');
});
});
From b062248ca73431325f72a9f02611534af25c0f5e Mon Sep 17 00:00:00 2001
From: Burhan Nasir
Date: Tue, 28 Jan 2025 15:12:46 +0500
Subject: [PATCH 22/23] Cleanup for search template in git action
---
.github/workflows/cypress-tests.yml | 8 +++++++-
tests/cypress/integration/features/search-templates.cy.js | 3 ---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml
index 6f81857..a4b9243 100644
--- a/.github/workflows/cypress-tests.yml
+++ b/.github/workflows/cypress-tests.yml
@@ -174,8 +174,14 @@ jobs:
${{ github.workspace }}/tests/cypress/videos/
${{ github.workspace }}/tests/cypress/logs/
+ - name: Delete Elasticsearch search template
+ if: always()
+ run: |
+ ./bin/wp-env-cli tests-wordpress "wp --allow-root plugin activate elasticpress elasticpress-labs"
+ ./bin/wp-env-cli tests-wordpress "wp --allow-root elasticpress-tests delete-all-search-templates"
+
- name: Delete Elasticsearch indices
if: always()
run: |
./bin/wp-env-cli tests-wordpress "wp --allow-root plugin activate elasticpress elasticpress-labs"
- ./bin/wp-env-cli tests-wordpress "wp --allow-root elasticpress-tests delete-all-indices"
\ No newline at end of file
+ ./bin/wp-env-cli tests-wordpress "wp --allow-root elasticpress-tests delete-all-indices"
diff --git a/tests/cypress/integration/features/search-templates.cy.js b/tests/cypress/integration/features/search-templates.cy.js
index 4190015..4abed1c 100644
--- a/tests/cypress/integration/features/search-templates.cy.js
+++ b/tests/cypress/integration/features/search-templates.cy.js
@@ -185,8 +185,5 @@ describe('Search Templates Feature', () => {
cy.wait('@loadTemplateRequest');
cy.contains('It seems you have reached the limit of search').should('exist');
-
- // clean up
- cy.wpCli('wp elasticpress-tests delete-all-search-templates');
});
});
From d2877ecbdb13fd93fab145750c75b0d85e10863d Mon Sep 17 00:00:00 2001
From: Burhan Nasir
Date: Tue, 28 Jan 2025 15:17:43 +0500
Subject: [PATCH 23/23] Typo
---
.github/workflows/cypress-tests.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml
index a4b9243..b7c2f66 100644
--- a/.github/workflows/cypress-tests.yml
+++ b/.github/workflows/cypress-tests.yml
@@ -174,7 +174,7 @@ jobs:
${{ github.workspace }}/tests/cypress/videos/
${{ github.workspace }}/tests/cypress/logs/
- - name: Delete Elasticsearch search template
+ - name: Delete Elasticsearch search templates
if: always()
run: |
./bin/wp-env-cli tests-wordpress "wp --allow-root plugin activate elasticpress elasticpress-labs"