wip
This commit is contained in:
commit
2bf4ec01d0
|
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
[*.md]
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitmodules export-ignore
|
||||
.travis.yml export-ignore
|
||||
changelog.md export-ignore
|
||||
tests export-ignore
|
||||
phpunit.xml export-ignore
|
||||
js/*/src export-ignore
|
||||
js/*/Gulpfile.js
|
||||
js/*/package.json
|
||||
js/*/yarn.lock
|
||||
|
||||
js/*/dist/*.js -diff
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
name: Build JavaScript assets
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: flarum/action-build@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
js/node_modules
|
||||
js/dist
|
||||
vendor/
|
||||
composer.lock
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "blomstra/search",
|
||||
"description": "Replaces Flarum search with a fulltext search server.",
|
||||
"keywords": [
|
||||
"extension",
|
||||
"flarum", "blomstra",
|
||||
"search", "fulltext"
|
||||
],
|
||||
"support": {
|
||||
"email": "helpdesk@blomstra.net",
|
||||
"forum": "https://blomstra.community/t/ext-search"
|
||||
},
|
||||
"type": "flarum-extension",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniël Klabbers",
|
||||
"email": "daniel@klabbers.email",
|
||||
"homepage": "http://luceos.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">= 8.0",
|
||||
"flarum/core": "^1.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"meilisearch/meilisearch-php": "Required to use the MeiliSearch engine (^0.19)."
|
||||
},
|
||||
"extra": {
|
||||
"flarum-extension": {
|
||||
"title": "Search",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"image": "resources/assets/logo.svg",
|
||||
"backgroundColor": "#E3E7F1",
|
||||
"backgroundSize": "100%",
|
||||
"backgroundRepeat": "no-repeat",
|
||||
"backgroundPosition": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Blomstra\\Search\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search;
|
||||
|
||||
use Flarum\Extend as Flarum;
|
||||
|
||||
return [
|
||||
(new Flarum\ServiceProvider)->register(Provider::class),
|
||||
|
||||
(new Flarum\Frontend('forum'))
|
||||
->js(__DIR__ . '/js/dist/forum.js'),
|
||||
|
||||
(new Flarum\Routes('api'))
|
||||
->get('blomstra/search', 'blomstra.search', Api\Controllers\SearchController::class)
|
||||
];
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './src/forum';
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "@blomstra/realtime",
|
||||
"dependencies": {
|
||||
"flarum-webpack-config": "^1.0.0",
|
||||
"lodash-es": "^4.17.14",
|
||||
"pusher-js": "^7.0.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"lint": "prettier --single-quote --trailing-comma es5 --print-width 150 --tab-width 4 --write src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import app from 'flarum/app';
|
||||
import highlight from 'flarum/common/helpers/highlight';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import { SearchSource } from 'flarum/forum/components/Search';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* The `DiscussionsSearchSource` finds and displays discussion search results in
|
||||
* the search dropdown.
|
||||
*/
|
||||
export default class Source implements SearchSource {
|
||||
protected results = new Map<string, unknown[]>();
|
||||
|
||||
search(query: string) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
this.results.set(query, []);
|
||||
|
||||
const params = {
|
||||
filter: { q: query },
|
||||
page: { limit: 3 },
|
||||
include: 'mostRelevantPost',
|
||||
};
|
||||
|
||||
return app.request(
|
||||
app.forum.attribute('apiUrl') + '/blomstra/search',
|
||||
params
|
||||
).then((results) => this.results.set(query, results));
|
||||
}
|
||||
|
||||
view(query: string): Array<Mithril.Vnode> {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const results = (this.results.get(query) || []).map((discussion: unknown) => {
|
||||
const mostRelevantPost = discussion.mostRelevantPost();
|
||||
|
||||
return (
|
||||
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
|
||||
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
|
||||
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
|
||||
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}) as Array<Mithril.Vnode>;
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
|
||||
<li>
|
||||
<LinkButton icon="fas fa-search" href={app.route('index', { q: query })}>
|
||||
{app.translator.trans('core.forum.search.all_discussions_button', { query })}
|
||||
</LinkButton>
|
||||
</li>,
|
||||
...results,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import app from 'flarum/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import Search from 'flarum/forum/components/Search';
|
||||
import Source from './Source';
|
||||
|
||||
app.initializers.add('blomstra-search', () => {
|
||||
extend(Search.prototype, 'sourceItems', function (items) {
|
||||
items.remove('users');
|
||||
items.remove('discussions');
|
||||
|
||||
items.add('discussions', new Source());
|
||||
|
||||
console.log(items)
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
const config = require('flarum-webpack-config');
|
||||
|
||||
module.exports = config();
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search\Api\Controllers;
|
||||
|
||||
use Flarum\Api\Controller\AbstractListController;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class SearchController extends AbstractListController
|
||||
{
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
// TODO: Implement data() method.
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class Mapping extends Collection
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search\Observe;
|
||||
|
||||
use Blomstra\Search\Mapping;
|
||||
use Blomstra\Search\Searchables\Searchable;
|
||||
use Flarum\Queue\AbstractJob;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use MeiliSearch\Client;
|
||||
|
||||
class Job extends AbstractJob
|
||||
{
|
||||
public function __construct(protected Model $model)
|
||||
{}
|
||||
|
||||
public function handle(Client $meili, Mapping $mapping)
|
||||
{
|
||||
$map = $mapping->get(get_class($this->model));
|
||||
|
||||
/** @var Searchable $searchable */
|
||||
$searchable = new $map['searchable']($this->model);
|
||||
|
||||
$body = array_merge([
|
||||
$searchable->fulltext()
|
||||
], [
|
||||
$this->model->getKeyName() => $this->model->getKey()
|
||||
]);
|
||||
|
||||
$meili->index($map['index'])->addDocuments(
|
||||
[$body],
|
||||
$this->model->getKeyName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search\Observe;
|
||||
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
|
||||
class Observer
|
||||
{
|
||||
|
||||
public function saved($model)
|
||||
{
|
||||
$this->queue()->push(new Job($model));
|
||||
}
|
||||
|
||||
public function deleted($model)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected function queue(): Queue
|
||||
{
|
||||
return resolve(Queue::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search;
|
||||
|
||||
use Blomstra\Search\Observe\Observer;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Post\CommentPost;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Support\Arr;
|
||||
use MeiliSearch\Client;
|
||||
|
||||
class Provider extends AbstractServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
$this->container->singleton(Mapping::class, function () {
|
||||
return new Mapping([
|
||||
\Flarum\Discussion\Discussion::class => [
|
||||
'index' => 'discussions',
|
||||
'searchable' => \Blomstra\Search\Searchables\Discussion::class
|
||||
],
|
||||
CommentPost::class => [
|
||||
'index' => 'posts',
|
||||
'searchable' => null,
|
||||
]
|
||||
]);
|
||||
});
|
||||
|
||||
$this->container->singleton(Client::class, function (Container $container) {
|
||||
$config = $container->make('flarum.config') ?? [];
|
||||
|
||||
$meili = Arr::get($config, 'meili');
|
||||
|
||||
if (! $meili) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Client($meili);
|
||||
});
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
/** @var Mapping $searchables */
|
||||
$searchables = $this->container->get(Mapping::class);
|
||||
|
||||
$searchables->each(function ($mapped, $model) {
|
||||
forward_static_call(
|
||||
[$model, 'observe'],
|
||||
Observer::class
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search\Searchables;
|
||||
|
||||
use Flarum\Discussion\Discussion as Model;
|
||||
use Flarum\Tags\Tag;
|
||||
|
||||
class Discussion extends Searchable
|
||||
{
|
||||
public function __construct(
|
||||
protected Model $discussion
|
||||
) {}
|
||||
|
||||
public function filters(): ?array
|
||||
{
|
||||
if ($this->extensionEnabled('flarum-tags')) {
|
||||
return $this->discussion->tags->map(function (Tag $tag) {
|
||||
return "tag:$tag->id";
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fulltext(): ?array
|
||||
{
|
||||
return [
|
||||
'title' => $this->discussion->title,
|
||||
'content' => $this->discussion->firstPost->content
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Blomstra\Search\Searchables;
|
||||
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
|
||||
abstract class Searchable
|
||||
{
|
||||
/**
|
||||
* Returns an array of filterable attributes.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
abstract public function filters(): ?array;
|
||||
|
||||
/**
|
||||
* An array of strings with which fulltext matching can be executed.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
abstract public function fulltext(): ?array;
|
||||
|
||||
protected function extensionEnabled(string $extension): bool
|
||||
{
|
||||
/** @var ExtensionManager $manager */
|
||||
$manager = resolve(ExtensionManager::class);
|
||||
|
||||
return $manager->isEnabled($extension);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue