Skip to content

Commit

Permalink
Merge pull request #16 from Setono/autcomplete
Browse files Browse the repository at this point in the history
Add autocomplete functionality for the search input
  • Loading branch information
Zales0123 authored Sep 6, 2024
2 parents 3fc1872 + 85242e3 commit 41e603b
Show file tree
Hide file tree
Showing 19 changed files with 384 additions and 6 deletions.
28 changes: 26 additions & 2 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,15 @@ public function getConfigTreeBuilder(): TreeBuilder
->cannotBeEmpty()
->end()
->scalarNode('master_key')
->info('This is the master key for the Meilisearch instance')
->info('This is the master API key for the Meilisearch instance')
->defaultValue('%env(MEILISEARCH_MASTER_KEY)%')
->cannotBeEmpty()
->end()
->scalarNode('search_key')
->info('This is the search API key for the Meilisearch instance')
->defaultValue('%env(MEILISEARCH_SEARCH_KEY)%')
->cannotBeEmpty()
->end()
->end()
->end()
->arrayNode('search')
Expand All @@ -109,7 +114,26 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->scalarNode('index')
->info('The index to search (must be configured in setono_sylius_meilisearch.indexes)')
->isRequired()
->cannotBeEmpty()
->end()
->end()
->end()
->arrayNode('autocomplete')
->canBeEnabled()
->info('Configures the autocomplete feature')
->children()
->arrayNode('indexes')
->requiresAtLeastOneElement()
->scalarPrototype()->end()
->end()
->scalarNode('container')
->defaultValue('#autocomplete')
->info('This is the javascript selector for the HTML element that will contain the autocomplete')
->cannotBeEmpty()
->end()
->scalarNode('placeholder')
->defaultValue('setono_sylius_meilisearch.ui.search_placeholder')
->info('This is the placeholder text that will be displayed in the input field')
->cannotBeEmpty()
;

Expand Down
61 changes: 58 additions & 3 deletions src/DependencyInjection/SetonoSyliusMeilisearchExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Setono\SyliusMeilisearchPlugin\Indexer\DefaultIndexer;
use Setono\SyliusMeilisearchPlugin\Indexer\IndexerInterface;
use Setono\SyliusMeilisearchPlugin\Provider\IndexScope\IndexScopeProviderInterface;
use Setono\SyliusMeilisearchPlugin\Twig\AutocompleteRuntime;
use Setono\SyliusMeilisearchPlugin\UrlGenerator\EntityUrlGeneratorInterface;
use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension;
use Sylius\Bundle\ResourceBundle\SyliusResourceBundle;
Expand All @@ -42,8 +43,9 @@ public function load(array $configs, ContainerBuilder $container): void
*
* @var array{
* indexes: array<string, array{document: class-string<Document>, entities: list<class-string>, data_provider: class-string, indexer: class-string|null, prefix: string|null, default_filters: array<string, bool>}>,
* server: array{ host: string, master_key: string },
* server: array{ host: string, master_key: string, search_key: string },
* search: array{ enabled: bool, path: string, index: string, hits_per_page: int },
* autocomplete: array{ enabled: bool, indexes: list<string>, container: string, placeholder: string },
* resources: array,
* } $config
*/
Expand All @@ -55,6 +57,7 @@ public function load(array $configs, ContainerBuilder $container): void
// server
$container->setParameter('setono_sylius_meilisearch.server.host', $config['server']['host']);
$container->setParameter('setono_sylius_meilisearch.server.master_key', $config['server']['master_key']);
$container->setParameter('setono_sylius_meilisearch.server.search_key', $config['server']['search_key']);

$loader->load('services.xml');

Expand All @@ -73,6 +76,7 @@ public function load(array $configs, ContainerBuilder $container): void

self::registerIndexesConfiguration($config['indexes'], $container);
self::registerSearchConfiguration($config['search'], array_keys($config['indexes']), $container, $loader);
self::registerAutocompleteConfiguration($config['autocomplete'], array_keys($config['indexes']), $container, $loader);
}

public function prepend(ContainerBuilder $container): void
Expand Down Expand Up @@ -209,12 +213,27 @@ public function prepend(ContainerBuilder $container): void
'events' => [
'sylius.shop.layout.header.grid' => [
'blocks' => [
'search' => [
'setono_sylius_meilisearch_search' => [
'template' => '@SetonoSyliusMeilisearchPlugin/search/widget.html.twig',
'priority' => 20,
],
],
],
'sylius.shop.layout.javascripts' => [
'blocks' => [
'setono_sylius_meilisearch_autocomplete' => [
'template' => '@SetonoSyliusMeilisearchPlugin/autocomplete/javascript.html.twig',
],
],
],
'sylius.shop.layout.stylesheets' => [
'blocks' => [
'setono_sylius_meilisearch_styles' => [
'template' => '@SetonoSyliusMeilisearchPlugin/autocomplete/styles.html.twig',
'priority' => -20,
],
],
],
'sylius.shop.layout.after_body' => [
'blocks' => [
'setono_sylius_meilisearch_loader' => [
Expand Down Expand Up @@ -291,6 +310,7 @@ private static function registerDefaultIndexer(ContainerBuilder $container, stri
private static function registerSearchConfiguration(array $config, array $indexes, ContainerBuilder $container, LoaderInterface $loader): void
{
$container->setParameter('setono_sylius_meilisearch.search.enabled', $config['enabled']);
$container->setParameter('setono_sylius_meilisearch.search.path', $config['path']); // The route that uses this parameter is defined even if search is disabled

if (!$config['enabled']) {
return;
Expand All @@ -307,13 +327,48 @@ private static function registerSearchConfiguration(array $config, array $indexe
$container->setAlias('setono_sylius_meilisearch.index.search', self::getIndexServiceId($config['index']));
$container->setAlias(Index::class . ' $searchIndex', self::getIndexServiceId($config['index']));

$container->setParameter('setono_sylius_meilisearch.search.path', $config['path']);
$container->setParameter('setono_sylius_meilisearch.search.index', $config['index']);
$container->setParameter('setono_sylius_meilisearch.search.hits_per_page', $config['hits_per_page']);

$loader->load('services/conditional/search.xml');
}

/**
* @param array{ enabled: bool, indexes: list<string>, container: string, placeholder: string } $config
* @param list<string> $indexes a list of configured index names
*/
private static function registerAutocompleteConfiguration(array $config, array $indexes, ContainerBuilder $container, LoaderInterface $loader): void
{
$container->setParameter('setono_sylius_meilisearch.autocomplete.enabled', $config['enabled']);

if (!$config['enabled']) {
return;
}

if ([] === $config['indexes']) {
throw new \RuntimeException('You have to configure at least one index for the autocomplete');
}

foreach ($config['indexes'] as $index) {
if (!in_array($index, $indexes, true)) {
throw new \RuntimeException(sprintf('For the autocomplete configuration you have added the index "%s". That index is not configured in setono_sylius_meilisearch.indexes. Available indexes are [%s]', $index, implode(', ', $indexes)));
}
}

$container->setParameter('setono_sylius_meilisearch.autocomplete.container', $config['container']);
$container->setParameter('setono_sylius_meilisearch.autocomplete.placeholder', $config['placeholder']);

$loader->load('services/conditional/autocomplete.xml');

$container->getDefinition(AutocompleteRuntime::class)->setArgument(
'$indexes',
array_map(
static fn (string $index) => new Reference(self::getIndexServiceId($index)),
$config['indexes'],
),
);
}

private static function getIndexServiceId(string $indexName): string
{
return sprintf('setono_sylius_meilisearch.index.%s', $indexName);
Expand Down
23 changes: 23 additions & 0 deletions src/Meilisearch/Autocomplete/Configuration/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Autocomplete\Configuration;

final class Configuration
{
/** @var list<Source> */
public array $sources = [];

public function __construct(
public readonly string $host,
public readonly string $searchKey,

/** This is the javascript selector for the HTML element that will contain the autocomplete */
public readonly string $container,

/** This is the placeholder text that will be displayed in the input field */
public readonly string $placeholder,
) {
}
}
16 changes: 16 additions & 0 deletions src/Meilisearch/Autocomplete/Configuration/Source.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Meilisearch\Autocomplete\Configuration;

final class Source
{
public function __construct(
public readonly string $id,
public readonly string $index,
/** The attribute that holds the URL on any given item/document */
public readonly ?string $urlAttribute = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<import resource="services/normalizer.xml"/>
<import resource="services/provider.xml"/>
<import resource="services/resolver.xml"/>
<import resource="services/twig.xml"/>
<import resource="services/url_generator.xml"/>
</imports>
</container>
17 changes: 17 additions & 0 deletions src/Resources/config/services/conditional/autocomplete.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Setono\SyliusMeilisearchPlugin\Twig\AutocompleteRuntime">
<argument type="service" id="setono_sylius_meilisearch.resolver.index_name"/>
<argument type="service" id="translator"/>
<argument>%setono_sylius_meilisearch.server.host%</argument>
<argument>%setono_sylius_meilisearch.server.search_key%</argument>
<argument>%setono_sylius_meilisearch.autocomplete.container%</argument>
<argument>%setono_sylius_meilisearch.autocomplete.placeholder%</argument>
<argument type="collection" /> <!-- Gets set in the extension -->

<tag name="twig.runtime"/>
</service>
</services>
</container>
17 changes: 17 additions & 0 deletions src/Resources/config/services/twig.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Setono\SyliusMeilisearchPlugin\Twig\AutocompleteExtension">
<argument>%setono_sylius_meilisearch.autocomplete.enabled%</argument>

<tag name="twig.extension"/>
</service>

<service id="Setono\SyliusMeilisearchPlugin\Twig\SearchExtension">
<argument>%setono_sylius_meilisearch.search.enabled%</argument>

<tag name="twig.extension"/>
</service>
</services>
</container>
4 changes: 4 additions & 0 deletions src/Resources/public/css/algolia.autocomplete.css

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/Resources/public/js/algolia.autocomplete.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Resources/public/js/meilisearch.autocomplete.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/Resources/translations/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ setono_sylius_meilisearch:
indexes: Indexes
no_indexes: No indexes
search: Search
search_placeholder: Search...
search_results_for: 'Search results for'
search_term_and_synonym: 'Search term and synonym'
synonym: Synonym
Expand Down
69 changes: 69 additions & 0 deletions src/Resources/views/autocomplete/javascript.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% if ssm_autocomplete_enabled() %}
{# @var configuration \Setono\SyliusMeilisearchPlugin\Meilisearch\Autocomplete\Configuration\Configuration #}
{% set configuration = ssm_autocomplete_configuration() %}

{# todo should be saved on the individual sources #}
{% set templates = {
item: include('@SetonoSyliusMeilisearchPlugin/autocomplete/templates/item.html.twig'),
} %}
<script src="{{ asset('bundles/setonosyliusmeilisearchplugin/js/algolia.autocomplete.js') }}"></script>
<script src="{{ asset('bundles/setonosyliusmeilisearchplugin/js/meilisearch.autocomplete.js') }}"></script>
<script>
const { autocomplete } = window['@algolia/autocomplete-js'];
const { meilisearchAutocompleteClient, getMeilisearchResults } = window['@meilisearch/autocomplete-client'];
const searchClient = meilisearchAutocompleteClient({
url: '{{ configuration.host }}',
apiKey: '{{ configuration.searchKey }}'
});
autocomplete({
{% if app.debug -%}
debug: true,
{% endif -%}
container: '{{ configuration.container }}',
placeholder: '{{ configuration.placeholder }}',
{% if ssm_search_enabled() -%}
onSubmit({ event, state }) {
location.href = '{{ path('setono_sylius_meilisearch_shop_search') }}?q=' + state.query;
},
initialState: {
query: new URL(window.location).searchParams.get('q'),
},
{% endif -%}
getSources({ query }) {
return [
{% for source in configuration.sources %}
{
sourceId: '{{ source.id }}',
getItems() {
return getMeilisearchResults({
searchClient,
queries: [
{
indexName: '{{ source.index }}',
query,
},
],
})
},
templates: {
item({ item, components, html }) {
return html`{{ templates.item|raw }}`;
},
},
{% if source.urlAttribute -%}
onSelect({ item }) {
location.href = item.{{ source.urlAttribute }};
},
getItemUrl({ item }) {
return item.{{ source.urlAttribute }};
},
{% endif -%}
},
{% endfor %}
]
},
});
</script>
{% endif %}
8 changes: 8 additions & 0 deletions src/Resources/views/autocomplete/styles.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{%- if ssm_autocomplete_enabled() -%}
<link href="{{ asset('bundles/setonosyliusmeilisearchplugin/css/algolia.autocomplete.css') }}" rel="stylesheet">
<style>
.aa-Panel {
z-index: 9999;
}
</style>
{%- endif -%}
15 changes: 15 additions & 0 deletions src/Resources/views/autocomplete/templates/item.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div class="aa-ItemWrapper">
<div class="aa-ItemContent">
<div class="aa-ItemIcon aa-ItemIcon--alignTop">
<img src="${item.image}" alt="${item.name}" width="100"/>
</div>
<div class="aa-ItemContentBody">
<div class="aa-ItemContentTitle">
${components.Highlight({
hit: item,
attribute: 'name',
})}
</div>
</div>
</div>
</div>
8 changes: 7 additions & 1 deletion src/Resources/views/search/widget.html.twig
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
{{ render(path('setono_sylius_meilisearch_shop_search_widget')) }}
{% if ssm_autocomplete_enabled() %}
<div class="ui center aligned column">
<div id="autocomplete"></div>
</div>
{% elseif ssm_search_enabled() %}
{{ render(path('setono_sylius_meilisearch_shop_search_widget')) }}
{% endif %}
31 changes: 31 additions & 0 deletions src/Twig/AutocompleteExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Setono\SyliusMeilisearchPlugin\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class AutocompleteExtension extends AbstractExtension
{
public function __construct(private readonly bool $enabled)
{
}

/**
* @return list<TwigFunction>
*/
public function getFunctions(): array
{
return [
new TwigFunction('ssm_autocomplete_configuration', [AutocompleteRuntime::class, 'configuration']),

Check failure on line 22 in src/Twig/AutocompleteExtension.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: highest | SF~5.4.0)

InvalidArgument

src/Twig/AutocompleteExtension.php:22:64: InvalidArgument: Argument 2 of Twig\TwigFunction::__construct expects a public static callable, but a non-static callable provided (see https://psalm.dev/004)

Check failure on line 22 in src/Twig/AutocompleteExtension.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.1 | Deps: highest | SF~6.4.0)

InvalidArgument

src/Twig/AutocompleteExtension.php:22:64: InvalidArgument: Argument 2 of Twig\TwigFunction::__construct expects a public static callable, but a non-static callable provided (see https://psalm.dev/004)

Check failure on line 22 in src/Twig/AutocompleteExtension.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: highest | SF~5.4.0)

InvalidArgument

src/Twig/AutocompleteExtension.php:22:64: InvalidArgument: Argument 2 of Twig\TwigFunction::__construct expects a public static callable, but a non-static callable provided (see https://psalm.dev/004)

Check failure on line 22 in src/Twig/AutocompleteExtension.php

View workflow job for this annotation

GitHub Actions / Static Code Analysis (PHP8.2 | Deps: highest | SF~6.4.0)

InvalidArgument

src/Twig/AutocompleteExtension.php:22:64: InvalidArgument: Argument 2 of Twig\TwigFunction::__construct expects a public static callable, but a non-static callable provided (see https://psalm.dev/004)
new TwigFunction('ssm_autocomplete_enabled', $this->isEnabled(...)),
];
}

public function isEnabled(): bool
{
return $this->enabled;
}
}
Loading

0 comments on commit 41e603b

Please sign in to comment.