baseline
This commit is contained in:
121
pages/search/data().php
Normal file
121
pages/search/data().php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Router;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Support\SearchConfig;
|
||||
use MintyPHP\Service\PermissionService;
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\I18n;
|
||||
use MintyPHP\Repository\UserTenantRepository;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$userId = (int) ($_SESSION['user']['id'] ?? 0);
|
||||
if ($userId > 0) {
|
||||
PermissionService::getUserPermissions($userId);
|
||||
}
|
||||
|
||||
$query = trim((string) ($_GET['search'] ?? ''));
|
||||
if ($query === '') {
|
||||
Router::json(['data' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$limit = (int) ($_GET['limit'] ?? 10);
|
||||
if ($limit < 1) {
|
||||
$limit = 10;
|
||||
} elseif ($limit > 100) {
|
||||
$limit = 100;
|
||||
}
|
||||
$offset = (int) ($_GET['offset'] ?? 0);
|
||||
if ($offset < 0) {
|
||||
$offset = 0;
|
||||
}
|
||||
|
||||
$locale = I18n::$locale ?? (I18n::$defaultLocale ?? 'de');
|
||||
$tenantIds = $userId > 0 ? UserTenantRepository::listTenantIdsByUserId($userId) : [];
|
||||
$tenantScoped = !empty($tenantIds);
|
||||
|
||||
$tenantScopeFilters = SearchConfig::tenantScopeFilters();
|
||||
$resources = SearchConfig::resources($query, $locale);
|
||||
$items = [];
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$key = (string) ($resource['key'] ?? '');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$perm = (string) ($resource['permission'] ?? '');
|
||||
if ($perm !== '' && !PermissionService::userHas($userId, $perm)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tenantFilter = $tenantScopeFilters[$key] ?? '';
|
||||
$isTenantScoped = $tenantFilter !== '';
|
||||
if ($isTenantScoped && !$tenantScoped) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sql = $resource['resultSql'] ?? '';
|
||||
if ($sql === '') {
|
||||
continue;
|
||||
}
|
||||
$sql = str_replace('{{tenantFilter}}', $isTenantScoped ? $tenantFilter : '', $sql);
|
||||
$params = $resource['resultParams'] ?? [];
|
||||
if ($isTenantScoped && strpos($sql, '???') !== false) {
|
||||
$params[] = $tenantIds;
|
||||
}
|
||||
|
||||
$rows = DB::select($sql, ...$params);
|
||||
foreach ($rows as $row) {
|
||||
$data = $row;
|
||||
if (is_array($row) && count($row) === 1) {
|
||||
$data = reset($row);
|
||||
}
|
||||
$data = is_array($data) ? $data : [];
|
||||
$mapped = SearchConfig::mapResultItem($key, (string) ($resource['label'] ?? ''), $data);
|
||||
if ($mapped !== null) {
|
||||
$items[] = $mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$queryLower = mb_strtolower($query);
|
||||
$scoreItem = static function (array $item) use ($queryLower): int {
|
||||
$title = mb_strtolower((string) ($item['title'] ?? ''));
|
||||
$description = mb_strtolower((string) ($item['description'] ?? ''));
|
||||
if ($title === $queryLower) {
|
||||
return 400;
|
||||
}
|
||||
if ($title !== '' && strpos($title, $queryLower) === 0) {
|
||||
return 300;
|
||||
}
|
||||
if ($title !== '' && strpos($title, $queryLower) !== false) {
|
||||
return 200;
|
||||
}
|
||||
if ($description !== '' && strpos($description, $queryLower) !== false) {
|
||||
return 100;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
usort($items, static function (array $a, array $b) use ($scoreItem): int {
|
||||
$scoreA = $scoreItem($a);
|
||||
$scoreB = $scoreItem($b);
|
||||
if ($scoreA !== $scoreB) {
|
||||
return $scoreB <=> $scoreA;
|
||||
}
|
||||
$title = strcmp((string) ($a['title'] ?? ''), (string) ($b['title'] ?? ''));
|
||||
if ($title !== 0) {
|
||||
return $title;
|
||||
}
|
||||
return strcmp((string) ($a['type'] ?? ''), (string) ($b['type'] ?? ''));
|
||||
});
|
||||
|
||||
$total = count($items);
|
||||
$items = array_slice($items, $offset, $limit);
|
||||
|
||||
Router::json([
|
||||
'data' => $items,
|
||||
'total' => $total,
|
||||
]);
|
||||
17
pages/search/index().php
Normal file
17
pages/search/index().php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use MintyPHP\Buffer;
|
||||
use MintyPHP\Support\Guard;
|
||||
use MintyPHP\Session;
|
||||
|
||||
Guard::requireLogin();
|
||||
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
|
||||
Buffer::set('title', t('Search results'));
|
||||
Buffer::set('grid_lang', json_encode(gridLang(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
Buffer::set(
|
||||
'grid_csrf',
|
||||
json_encode(['key' => $csrfKey, 'token' => $csrfToken], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
86
pages/search/index(default).phtml
Normal file
86
pages/search/index(default).phtml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
use MintyPHP\Buffer;
|
||||
?>
|
||||
<div class="app-breadcrumb">
|
||||
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Search')); ?>
|
||||
</div>
|
||||
<div class="app-list-titlebar">
|
||||
<h1><?php e(t('Search results')); ?></h1>
|
||||
</div>
|
||||
<div class="app-list-toolbar">
|
||||
<label class="app-field">
|
||||
<span><?php e(t('Search')); ?></span>
|
||||
<input type="search" id="search-results-input" placeholder="<?php e(t('Search...')); ?>">
|
||||
</label>
|
||||
</div>
|
||||
<div class="app-list-table">
|
||||
<div id="search-results-grid"></div>
|
||||
</div>
|
||||
|
||||
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
|
||||
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||
<script type="module">
|
||||
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
|
||||
|
||||
const appBase = "<?php e(localeBase()); ?>";
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[char]));
|
||||
|
||||
const renderResult = (cell) => {
|
||||
const title = escapeHtml(cell?.title ?? '');
|
||||
const description = escapeHtml(cell?.description ?? '');
|
||||
const url = cell?.url ?? '#';
|
||||
const typeLabel = escapeHtml(cell?.type ?? '');
|
||||
const image = cell?.image ?? '';
|
||||
const icon = escapeHtml(cell?.icon ?? 'bi-search');
|
||||
const avatar = image
|
||||
? `<img class="search-result-avatar" src="${image}" alt="" loading="lazy">`
|
||||
: `<span class="search-result-avatar-placeholder"><i class="bi ${icon}"></i></span>`;
|
||||
|
||||
return gridjs.html(`
|
||||
<div class="search-result-row">
|
||||
${avatar}
|
||||
<div class="search-result-content">
|
||||
<a class="search-result-title" href="${url}">${title}</a>
|
||||
${description ? `<p class="search-result-desc">${description}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
};
|
||||
|
||||
const renderType = (cell) => gridjs.html(`<span class="search-result-type">${escapeHtml(cell ?? '')}</span>`);
|
||||
|
||||
createServerGrid({
|
||||
gridjs: window.gridjs,
|
||||
container: '#search-results-grid',
|
||||
dataUrl: 'search/data',
|
||||
appBase,
|
||||
columns: [
|
||||
{
|
||||
name: "<?php e(t('Result')); ?>",
|
||||
sort: false,
|
||||
formatter: (cell) => renderResult(cell)
|
||||
},
|
||||
{
|
||||
name: "<?php e(t('Type')); ?>",
|
||||
sort: false,
|
||||
formatter: (cell) => renderType(cell)
|
||||
}
|
||||
],
|
||||
sortColumns: [null, null],
|
||||
paginationLimit: 10,
|
||||
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
||||
mapData: (data) => data.data.map((row) => [row, row.type]),
|
||||
search: {
|
||||
input: '#search-results-input',
|
||||
param: 'search',
|
||||
debounce: 250
|
||||
},
|
||||
urlSync: true
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user