feat: new search WIP

This commit is contained in:
Barış Soner Uşaklı
2023-01-21 02:08:04 -05:00
parent 43c4877a82
commit 0f214b5078
3 changed files with 267 additions and 48 deletions

View File

@@ -1,21 +1,39 @@
{
"type-to-search": "Type to search",
"results_matching": "%1 result(s) matching \"%2\", (%3 seconds)",
"no-matches": "No matches found",
"advanced-search": "Advanced Search",
"in": "In",
"titles": "Titles",
"titles-posts": "Titles and Posts",
"in-titles": "In titles",
"in-titles-posts": "In titles and posts",
"in-posts": "In post",
"in-categories": "In categories",
"in-users": "In users",
"in-tags": "In tags",
"categories": "Categories",
"categories-x": "Categories: %!",
"type-a-category": "Type a category",
"tags": "Tags",
"tags-x": "Tags: %1",
"type-a-tag": "Type a tag",
"match-words": "Match words",
"match-all-words": "Match all words",
"match-any-word": "Match any word",
"all": "All",
"any": "Any",
"posted-by": "Posted by",
"in-categories": "In Categories",
"posted-by-usernames": "Posted by: %1",
"type-a-username": "Type a username",
"search-child-categories": "Search child categories",
"has-tags": "Has tags",
"reply-count": "Reply Count",
"replies": "Replies",
"replies-atleast-count": "Replies: At least %1",
"replies-atmost-count": "Replies: At most %1",
"at-least": "At least",
"at-most": "At most",
"relevance": "Relevance",
"time": "Time",
"post-time": "Post time",
"votes": "Votes",
"newer-than": "Newer than",
@@ -28,7 +46,22 @@
"three-months": "Three months",
"six-months": "Six months",
"one-year": "One year",
"time-newer-than-86400": "Time: Newer than yesterday",
"time-older-than-86400": "Time: Older than yesterday",
"time-newer-than-604800": "Time: Newer than one week",
"time-older-than-604800": "Time: Older than one week",
"time-newer-than-1209600": "Time: Newer than two weeks",
"time-older-than-1209600": "Time: Older than two weeks",
"time-newer-than-2592000": "Time: Newer than one month",
"time-older-than-2592000": "Time: Older than one month",
"time-newer-than-7776000": "Time: Newer than three months",
"time-older-than-7776000": "Time: Older than three months",
"time-newer-than-15552000": "Time: Newer than six months",
"time-older-than-15552000": "Time: Older than six months",
"time-newer-than-31104000": "Time: Newer than one year",
"time-older-than-31104000": "Time: Older than one year",
"sort-by": "Sort by",
"sort": "Sort",
"last-reply-time": "Last reply time",
"topic-title": "Topic title",
"topic-votes": "Topic votes",
@@ -39,11 +72,36 @@
"category": "Category",
"descending": "In descending order",
"ascending": "In ascending order",
"sort-by-relevance-desc": "Sort by: Relevance in descending order",
"sort-by-relevance-asc": "Sort by: Relevance in ascending order ",
"sort-by-timestamp-desc": "Sort by: Post time in descending order",
"sort-by-timestamp-asc": "Sort by: Post time in ascending order ",
"sort-by-votes-desc": "Sort by: Votes in descending order",
"sort-by-votes-asc": "Sort by: Votes in ascending order ",
"sort-by-topic.lastposttime-desc": "Sort by: Last reply time in descending order",
"sort-by-topic.lastposttime-asc": "Sort by: Last reply time in ascending order ",
"sort-by-topic.title-desc": "Sort by: Topic title in descending order",
"sort-by-topic.title-asc": "Sort by: Topic title in ascending order ",
"sort-by-topic.postcount-desc": "Sort by: Number of replies in descending order",
"sort-by-topic.postcount-asc": "Sort by: Number of replies in ascending order ",
"sort-by-topic.viewcount-desc": "Sort by: Number of views in descending order",
"sort-by-topic.viewcount-asc": "Sort by: Number of views in ascending order ",
"sort-by-topic.votes-desc": "Sort by: Topic votes in descending order",
"sort-by-topic.votes-asc": "Sort by: Topic votes in ascending order ",
"sort-by-topic.timestamp-desc": "Sort by: Topic start date in descending order",
"sort-by-topic.timestamp-asc": "Sort by: Topic start date in ascending order ",
"sort-by-user.username-desc": "Sort by: Username in descending order",
"sort-by-user.username-asc": "Sort by: Username in ascending order ",
"sort-by-category.name-desc": "Sort by: Category in descending order",
"sort-by-category.name-asc": "Sort by: Category in ascending order ",
"save": "Save",
"save-preferences": "Save preferences",
"clear-preferences": "Clear preferences",
"search-preferences-saved": "Search preferences saved",
"search-preferences-cleared": "Search preferences cleared",
"show-results-as": "Show results as",
"show-results-as-topics": "Show results as topics",
"show-results-as-posts": "Show results as posts",
"see-more-results": "See more results (%1)",
"search-in-category": "Search in \"%1\""
}

View File

@@ -3,25 +3,29 @@
define('forum/search', [
'search',
'autocomplete',
'storage',
'hooks',
'alerts',
], function (searchModule, autocomplete, storage, hooks, alerts) {
'api',
'translator',
'slugify',
], function (searchModule, storage, hooks, alerts, api, translator, slugify) {
const Search = {};
let selectedUsers = [];
Search.init = function () {
const searchQuery = $('#results').attr('data-search-query');
const searchIn = $('#search-in');
searchIn.on('change', function () {
updateFormItemVisiblity(searchIn.val());
});
searchModule.highlightMatches(searchQuery, $('.search-result-text p, .search-result-text.search-result-title a'));
const searchQuery = $('#results').attr('data-search-query');
searchModule.highlightMatches(
searchQuery,
$('.search-results .content p, .search-results .topic-title')
);
$('#advanced-search').off('submit').on('submit', function (e) {
$('#advanced-search form').off('submit').on('submit', function (e) {
e.preventDefault();
searchModule.query(getSearchDataFromDOM(), function () {
$('#search-input').val('');
@@ -33,9 +37,70 @@ define('forum/search', [
enableAutoComplete();
userFilterDropdown($('[component="user/filter"]'), ajaxify.data.userFilterSelected);
$('[component="search/filters"]').on('hidden.bs.dropdown', '.dropdown', function () {
const updateFns = {
replies: updateReplyCountFilter,
time: updateTimeFilter,
sort: updateSortFilter,
user: updateUserFilter,
};
if (updateFns[$(this).attr('data-filter-name')]) {
updateFns[$(this).attr('data-filter-name')]();
}
});
fillOutForm();
};
function updateUserFilter() {
const isActive = selectedUsers.length > 0;
let labelText = '[[search:posted-by]]';
if (selectedUsers.length) {
labelText = translator.compile(
'search:posted-by-usernames', selectedUsers.map(u => u.username).join(', ')
);
}
$('[component="user/filter/button"]').toggleClass(
'active-filter', isActive
).find('.filter-label').translateText(labelText);
}
function updateTimeFilter() {
const isActive = $('#post-time-range').val() > 0;
$('#post-time-button').toggleClass(
'active-filter', isActive
).find('.filter-label').translateText(
isActive ?
`[[search:time-${$('#post-time-filter').val()}-than-${$('#post-time-range').val()}]]` :
`[[search:time]]`
);
}
function updateSortFilter() {
const isActive = $('#post-sort-by').val() !== 'relevance' || $('#post-sort-direction').val() !== 'desc';
$('#sort-by-button').toggleClass(
'active-filter', isActive
).find('.filter-label').translateText(
isActive ?
`[[search:sort-by-${$('#post-sort-by').val()}-${$('#post-sort-direction').val()}]]` :
`[[search:sort]]`
);
}
function updateReplyCountFilter() {
const isActive = $('#reply-count').val() > 0;
$('#reply-count-button').toggleClass(
'active-filter', isActive
).find('.filter-label').translateText(
isActive ?
`[[search:replies-${$('#reply-count-filter').val()}-count, ${$('#reply-count').val()}]]` :
`[[search:replies]]`
);
}
function getSearchDataFromDOM() {
const form = $('#advanced-search');
const searchData = {
@@ -44,7 +109,7 @@ define('forum/search', [
searchData.term = $('#search-input').val();
if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') {
searchData.matchWords = form.find('#match-words-filter').val();
searchData.by = form.find('#posted-by-user').tagsinput('items');
searchData.by = selectedUsers.length ? selectedUsers.map(u => u.username) : undefined;
searchData.categories = form.find('#posted-in-categories').val();
searchData.searchChildren = form.find('#search-children').is(':checked');
searchData.hasTags = form.find('#has-tags').tagsinput('items');
@@ -54,7 +119,7 @@ define('forum/search', [
searchData.timeRange = form.find('#post-time-range').val();
searchData.sortBy = form.find('#post-sort-by').val();
searchData.sortDirection = form.find('#post-sort-direction').val();
searchData.showAs = form.find('#show-as-topics').is(':checked') ? 'topics' : 'posts';
searchData.showAs = form.find('#show-results-as').val();
}
hooks.fire('action:search.getSearchDataFromDOM', {
@@ -66,8 +131,8 @@ define('forum/search', [
}
function updateFormItemVisiblity(searchIn) {
const hide = searchIn.indexOf('posts') === -1 && searchIn.indexOf('titles') === -1;
$('.post-search-item').toggleClass('hide', hide);
const hideTitlePostFilters = !searchIn.includes('posts') && !searchIn.includes('titles');
$('.post-search-item').toggleClass('hidden', hideTitlePostFilters);
}
function fillOutForm() {
@@ -90,6 +155,10 @@ define('forum/search', [
$('#match-words-filter').val(formData.matchWords);
}
if (formData.showAs) {
$('#show-results-as').val(formData.showAs);
}
if (formData.by) {
formData.by = Array.isArray(formData.by) ? formData.by : [formData.by];
formData.by.forEach(function (by) {
@@ -127,13 +196,6 @@ define('forum/search', [
}
$('#post-sort-direction').val(formData.sortDirection || 'desc');
if (formData.showAs) {
const isTopic = formData.showAs === 'topics';
const isPost = formData.showAs === 'posts';
$('#show-as-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic);
$('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost);
}
hooks.fire('action:search.fillOutForm', {
form: formData,
});
@@ -147,36 +209,103 @@ define('forum/search', [
return false;
});
$('#clear-preferences').on('click', function () {
$('#clear-preferences').on('click', async function () {
storage.removeItem('search-preferences');
const query = $('#search-input').val();
$('#advanced-search')[0].reset();
$('#search-input').val(query);
const html = await app.parseAndTranslate('partials/search-filters', {});
$('[component="search/filters"]').replaceWith(html);
// clearing dom removes all event handlers, reinitialize
userFilterDropdown($('[component="user/filter"]'), []);
alerts.success('[[search:search-preferences-cleared]]');
return false;
});
}
function enableAutoComplete() {
const userEl = $('#posted-by-user');
userEl.tagsinput({
tagClass: 'badge bg-info',
confirmKeys: [13, 44],
trimValue: true,
});
if (app.user.privileges['search:users']) {
autocomplete.user(userEl.siblings('.bootstrap-tagsinput').find('input'));
function userFilterDropdown(el, _selectedUsers) {
selectedUsers = _selectedUsers;
async function renderSelectedUsers() {
const html = await app.parseAndTranslate('partials/search-filters', 'userFilterSelected', {
userFilterSelected: selectedUsers,
});
el.find('[component="user/filter/selected"]').html(html);
}
const tagEl = $('#has-tags');
tagEl.tagsinput({
tagClass: 'badge bg-info',
confirmKeys: [13, 44],
trimValue: true,
});
if (app.user.privileges['search:tags']) {
autocomplete.tag(tagEl.siblings('.bootstrap-tagsinput').find('input'));
async function doSearch() {
let result = { users: [] };
const query = el.find('[component="user/filter/search"]').val();
if (query && query.length > 1) {
if (app.user.privileges['search:users']) {
result = await api.get('/api/users', { query: query });
} else {
try {
const userData = await api.get(`/api/user/${slugify(query)}`);
result.users.push(userData);
} catch (err) {}
}
}
if (!result.users.length) {
el.find('[component="user/filter/results"]').translateHtml(
'[[users:no-users-found]]'
);
return;
}
result.users = result.users.slice(0, 20);
const html = await app.parseAndTranslate('partials/search-filters', 'userFilterResults', {
userFilterResults: result.users,
});
const uidToUser = {};
result.users.forEach((user) => {
uidToUser[user.uid] = user;
});
el.find('[component="user/filter/results"]').html(html);
el.find('[component="user/filter/results"] [data-uid]').on('click', async function () {
selectedUsers.push(uidToUser[$(this).attr('data-uid')]);
renderSelectedUsers();
});
}
el.find('[component="user/filter/search"]').on('keyup', utils.debounce(function () {
if (app.user.privileges['search:users']) {
doSearch();
}
}, 1000));
el.on('click', '[component="user/filter/delete"]', function () {
const uid = $(this).attr('data-uid');
selectedUsers = selectedUsers.filter(u => parseInt(u.uid, 10) !== parseInt(uid, 10));
renderSelectedUsers();
});
el.find('[component="user/filter/search"]').on('keyup', (e) => {
if (e.key === 'Enter' && !app.user.privileges['search:users']) {
doSearch();
}
});
el.on('shown.bs.dropdown', function () {
el.find('[component="user/filter/search"]').trigger('focus');
});
}
function enableAutoComplete() {
// const userEl = $('#posted-by-user');
// userEl.tagsinput({
// tagClass: 'badge bg-info',
// confirmKeys: [13, 44],
// trimValue: true,
// });
// if (app.user.privileges['search:users']) {
// autocomplete.user(userEl.siblings('.bootstrap-tagsinput').find('input'));
// }
// const tagEl = $('#has-tags');
// tagEl.tagsinput({
// tagClass: 'badge bg-info',
// confirmKeys: [13, 44],
// trimValue: true,
// });
// if (app.user.privileges['search:tags']) {
// autocomplete.tag(tagEl.siblings('.bootstrap-tagsinput').find('input'));
// }
}
return Search;

View File

@@ -8,8 +8,10 @@ const meta = require('../meta');
const plugins = require('../plugins');
const search = require('../search');
const categories = require('../categories');
const user = require('../user');
const pagination = require('../pagination');
const privileges = require('../privileges');
const translator = require('../translator');
const utils = require('../utils');
const helpers = require('./helpers');
@@ -57,12 +59,12 @@ searchController.search = async function (req, res, next) {
categories: req.query.categories,
searchChildren: req.query.searchChildren,
hasTags: req.query.hasTags,
replies: req.query.replies,
repliesFilter: req.query.repliesFilter,
timeRange: req.query.timeRange,
timeFilter: req.query.timeFilter,
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
sortDirection: req.query.sortDirection,
replies: validator.escape(String(req.query.replies || '')),
repliesFilter: validator.escape(String(req.query.repliesFilter || '')),
timeRange: validator.escape(String(req.query.timeRange || '')),
timeFilter: validator.escape(String(req.query.timeFilter || '')),
sortBy: validator.escape(String(req.query.sortBy || '')) || meta.config.searchDefaultSortBy || '',
sortDirection: validator.escape(String(req.query.sortDirection || '')),
page: page,
itemsPerPage: req.query.itemsPerPage,
uid: req.uid,
@@ -94,6 +96,28 @@ searchController.search = async function (req, res, next) {
searchData.showAsTopics = req.query.showAs === 'topics';
searchData.title = '[[global:header.search]]';
searchData.filters = {
replies: {
active: !!data.repliesFilter,
label: `[[search:replies-${data.repliesFilter}-count, ${data.replies}]]`,
},
time: {
active: !!(data.timeFilter && data.timeRange),
label: `[[search:time-${data.timeFilter}-than-${data.timeRange}]]`,
},
sort: {
active: !!(data.sortBy && data.sortBy !== 'relevance'),
label: `[[search:sort-by-${data.sortBy}-${data.sortDirection}]]`,
},
users: {
active: !!(data.postedBy),
label: translator.compile(
'search:posted-by-usernames',
(Array.isArray(data.postedBy) ? data.postedBy : []).map(u => validator.escape(String(u))).join(', ')
),
},
};
searchData.userFilterSelected = await getSelectedUsers(data.postedBy);
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
searchData.searchDefaultIn = meta.config.searchDefaultIn || 'titlesposts';
searchData.privileges = userPrivileges;
@@ -127,6 +151,14 @@ async function recordSearch(data) {
}
}
async function getSelectedUsers(postedBy) {
if (!Array.isArray(postedBy)) {
return [];
}
const uids = await user.getUidsByUsernames(postedBy);
return await user.getUsersFields(uids, ['username', 'userslug', 'picture']);
}
async function buildCategories(uid, searchOnly) {
if (searchOnly) {
return [];