Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions app/Http/Requests/UserListRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public function rules()
{
return [
'orderBy' => 'in:first_name,last_name,organisation,industry_type,created_at,last_logged_in_at',
'sort' => 'in:asc,desc'
'sort' => 'in:asc,desc',
'filters.search' => 'nullable|string|min:3|max:191'
];
}

Expand All @@ -36,8 +37,10 @@ public function getUserQuery(): UserQuery
$userQuery->setOrderBy($orderBy);
$userQuery->setSort($sort);

foreach ($this->get('filters', []) as $column => $value) {
$userQuery->addFilter($column, $value);
$filters = $this->get('filters', $this->get('filters\\', []));

foreach ($filters as $column => $value) {
$userQuery->addFilter(rtrim($column, '\\'), $value);
}

return $userQuery;
Expand Down
20 changes: 19 additions & 1 deletion app/Repositories/Access/UserRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ public function queryUsers(UserQuery $userQuery): Builder
});
});

$search = trim($userQuery->getFilters()->get('search', ''));
if (mb_strlen($search) >= 3) {
$this->applySearchFilter($builder, $search);
}

$society = $userQuery->getFilters()->only(['society'])->first();
if ($society) {
$builder->whereHas('organisations', function ($query) use ($society) {
Expand Down Expand Up @@ -214,12 +219,25 @@ public function queryUsers(UserQuery $userQuery): Builder
}

if (in_array($userQuery->getOrderBy(), ['first_name', 'last_name', 'organisation', 'industry_type'])) {
$builder->orderByRaw('(SELECT ' . $userQuery->getOrderBy() . ' FROM user_profiles WHERE user_profiles.user_id = users.id) ' . $userQuery->getSort());
$builder->orderByRaw('(SELECT ' . $userQuery->getOrderBy() . ' FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL ORDER BY user_profiles.id DESC LIMIT 1) ' . $userQuery->getSort());
}

return $builder;
}

private function applySearchFilter(Builder $builder, string $search)
{
$profileMatch = 'MATCH(user_profiles.first_name, user_profiles.last_name) AGAINST (? IN NATURAL LANGUAGE MODE)';
$emailMatch = 'MATCH(users.email) AGAINST (? IN NATURAL LANGUAGE MODE)';
$profileScore = "(SELECT {$profileMatch} FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL ORDER BY user_profiles.id DESC LIMIT 1)";
$score = "({$emailMatch} + COALESCE({$profileScore}, 0))";

$builder->select('users.*')
->selectRaw("{$score} as search_score", [$search, $search])
->whereRaw("({$emailMatch} > 0 OR EXISTS (SELECT 1 FROM user_profiles WHERE user_profiles.user_id = users.id AND user_profiles.deleted_at IS NULL AND {$profileMatch} > 0))", [$search, $search])
->orderBy('search_score', 'desc');
}

public function deactivate(User $user)
{
$user->activated = false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

class AddFulltextIndexesForUserSearch extends Migration
{

public function up()
{
if (env('DB_CONNECTION') !== 'mysql') {
return;
}

if (!$this->indexExists('users', 'users_email_fulltext_search')) {
DB::statement('ALTER TABLE users ADD FULLTEXT users_email_fulltext_search(email)');
}

$this->dropIndexIfExists('user_profiles', 'user_profiles_search_fulltext');
DB::statement('ALTER TABLE user_profiles ADD FULLTEXT user_profiles_search_fulltext(first_name, last_name)');
}


public function down()
{
if (env('DB_CONNECTION') !== 'mysql') {
return;
}

$this->dropIndexIfExists('users', 'users_email_fulltext_search');
$this->dropIndexIfExists('user_profiles', 'user_profiles_search_fulltext');
}

private function dropIndexIfExists(string $table, string $index)
{
if ($this->indexExists($table, $index)) {
DB::statement("ALTER TABLE {$table} DROP INDEX {$index}");
}
}

private function indexExists(string $table, string $index): bool
{
return (bool) DB::select(
DB::raw(
"SHOW KEYS
FROM {$table}
WHERE Key_name='{$index}'"
)
);
}
}
54 changes: 51 additions & 3 deletions resources/assets/js/pages/users/list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
:staynull="true"
v-if="!apiUsers"/>
</b-col>
<b-col cols="12" md="6" lg="4" xl="3" class="ml-lg-auto">
<p class="select-header"> {{ $t('users.list.search') }}</p>
<b-form-input
v-model.trim="searchFilter"
class="search-filter-input"
type="text"
:disabled="fetchingUsers"
:placeholder="$t('users.list.search_placeholder')">
</b-form-input>
</b-col>
<b-col class="text-right">
<b-button @click="clearFilters" :disabled="noFilters" class="btn-outline-primary clear-filter-btn">
{{ $t('users.list.clear_filters') }}
Expand Down Expand Up @@ -226,6 +236,7 @@ export default {
fetchingUsers: false,
roles: null,
locationOptions: null,
searchDebounce: null,
countries: require('country-list')()
}
},
Expand All @@ -239,6 +250,21 @@ export default {
deep: true
},
activatedFilter: fetchHandler,
searchFilter: {
handler (val, oldVal) {
if (val !== oldVal) {
clearTimeout(this.searchDebounce)
const search = val ? val.trim() : ''
if (search.length > 0 && search.length < 3) {
return
}
this.searchDebounce = setTimeout(() => {
this.currentPage = 1
this.fetchUsers()
}, 350)
}
}
},
roleFilter: fetchHandler,
countryFilter: fetchHandler,
termsFilter: fetchHandler,
Expand All @@ -259,6 +285,9 @@ export default {
this.fetchUsers()
this.fetchTerms()
},
beforeDestroy () {
clearTimeout(this.searchDebounce)
},
metaInfo () {
return { title: this.$t('users.list.manage') }
},
Expand All @@ -268,6 +297,7 @@ export default {
this.roleFilter = null
this.countryFilter = null
this.selectedSoc = null
this.searchFilter = ''
this.termsFilter = termsDefault
},
getSocietyByCode (code) {
Expand Down Expand Up @@ -310,12 +340,14 @@ export default {
},
async fetchUsers () {
this.fetchingUsers = true
await this.fetchRoles()
if (this.rolesEmpty) {
await this.fetchRoles()
}

let apiUserRole = this.roleOptions.find(role => role.name === 'API User')
const apiUserRole = this.roleOptions.find(role => role.name === 'API User')
let filterRoleId = this.roleFilter
if (filterRoleId === null) {
filterRoleId = this.apiUsers ? apiUserRole.id : null
filterRoleId = this.apiUsers && apiUserRole ? apiUserRole.id : null
}

if (!apiUserRole && this.apiUsers) {
Expand All @@ -330,6 +362,7 @@ export default {
role: filterRoleId,
society: this.selectedSoc ? this.selectedSoc.countryCode : null,
country_code: this.countryFilter,
search: this.searchFilter,
terms_version: this.termsFilter === termsDefault ? null : this.termsFilter
},
admin: !this.apiUsers && filterRoleId === null,
Expand Down Expand Up @@ -408,6 +441,14 @@ export default {
return this.$store.state.users.filters.roleFilter
}
},
searchFilter: {
set: function (newVal) {
this.$store.dispatch('users/setFilter', { searchFilter: newVal })
},
get: function () {
return this.$store.state.users.filters.searchFilter
}
},
countryFilter: {
set: function(newVal) {
this.$store.dispatch('users/setFilter', { countryFilter: newVal })
Expand Down Expand Up @@ -451,6 +492,7 @@ export default {
this.roleFilter === null &&
this.countryFilter === null &&
this.selectedSoc === null &&
!this.searchFilter &&
this.termsFilter === termsDefault
},
...mapGetters({
Expand Down Expand Up @@ -487,5 +529,11 @@ export default {
.text-nowrap {
white-space: nowrap;
}
.search-filter-input {
background: #E9E9E9;
border: none;
border-radius: 10px;
height: 2.45rem;
}

</style>
4 changes: 4 additions & 0 deletions resources/assets/js/plugins/axios.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ axios.interceptors.request.use(request => {

// Response interceptor
axios.interceptors.response.use(response => response, error => {
if (axios.isCancel(error)) {
return Promise.reject(error)
}

if (error.response && error.response.status) {
const { status } = error.response

Expand Down
13 changes: 13 additions & 0 deletions resources/assets/js/store/modules/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import axios from 'axios'
import * as types from '../mutation-types'
import querystring from 'querystring'

let latestFetchUsersRequest = 0

// state
export const state = {
users: {
Expand All @@ -24,6 +26,7 @@ export const state = {
roleFilter: null,
countryFilter: null,
selectedSoc: null,
searchFilter: '',
termsFilter: 'Terms and Conditions'
}
}
Expand Down Expand Up @@ -104,6 +107,7 @@ export const actions = {
roleFilter: null,
countryFilter: null,
selectedSoc: null,
searchFilter: '',
termsFilter: 'Terms and Conditions'
})
commit(types.SET_ORDER_BY, null)
Expand All @@ -120,6 +124,7 @@ export const actions = {
commit(types.SET_SORT_DESC, sortDesc)
},
async fetchUsers ({ commit }, { page, filters, excludes, admin, orderBy, sort }) {
const requestId = ++latestFetchUsersRequest
const queryOptions = { page }
if (orderBy !== null) {
queryOptions.orderBy = orderBy
Expand All @@ -131,11 +136,19 @@ export const actions = {
filterString += filters.society !== null ? `&filters[society]=${filters.society}` : ''
filterString += filters.country_code !== null ? `&filters[country_code]=${filters.country_code}` : ''
filterString += filters.terms_version !== null ? `&filters[terms_version]=${filters.terms_version}` : ''
const search = filters.search ? filters.search.trim() : ''
filterString += search.length >= 3 ? `&filters[search]=${encodeURIComponent(search)}` : ''

const url = admin ? '/api/users/admins' : '/api/users'
const { data } = await axios.get(`${url}?${querystring.stringify(queryOptions)}${filterString}`)
if (requestId !== latestFetchUsersRequest) {
return
}
commit(types.FETCH_USERS_SUCCESS, { users: data })
} catch (e) {
if (requestId !== latestFetchUsersRequest) {
return
}
commit(types.FETCH_USERS_FAILURE, { error: e })
}
},
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "ሀገር ምረጥ",
"select_terms": "የውሎች ስሪት ምረጥ",
"select_society": "የብሔራዊ ማህበር ምረጥ",
"search": "ፈልግ",
"search_placeholder": "ስም ወይም ኢሜይል (ቢያንስ 3 ቁምፊዎች)",
"create": "አዲስ መጠቀሚያ ይፍጠሩ፤",
"download_report": "ሪፖርት ያውርዱ፤",
"empty": "ምንም መጠቀሚያ ማግኘት አልተቻለም፤",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@
"select_country": "اختر بلدًا",
"select_terms": "اختر إصدار الشروط",
"select_society": "اختر جمعية وطنية",
"search": "بحث",
"search_placeholder": "الاسم أو البريد الإلكتروني (3 أحرف على الأقل)",
"create": "إنشاء مستخدم جديد",
"download_report": "تنزيل التقرير",
"empty": "لم يتم العثور على أي مستخدمين",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "একটি দেশ নির্বাচন করুন",
"select_terms": "একটি শর্তাবলী সংস্করণ নির্বাচন করুন",
"select_society": "জাতীয় সমিতি নির্বাচন করুন",
"search": "অনুসন্ধান",
"search_placeholder": "নাম বা ইমেল (কমপক্ষে ৩ অক্ষর)",
"create": "নতুন ব্যবহারকারী তৈরি করুন",
"download_report": "রিপোর্ট ডাউনলোড করুন",
"empty": "কোন ব্যবহারকারী খুঁজে পাইনি",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "Land auswählen",
"select_terms": "Version der Bedingungen auswählen",
"select_society": "Nationale Gesellschaft auswählen",
"search": "Suchen",
"search_placeholder": "Name oder E-Mail (mind. 3 Zeichen)",
"create": "Neuen Benutzer anlegen",
"download_report": "Bericht herunterladen",
"empty": "Es konnten keine Benutzer gefunden werden",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@
"select_country": "Select a country",
"select_terms": "Select a terms version",
"select_society": "Select a nacional society",
"search": "Search",
"search_placeholder": "Name or email (min. 3 characters)",
"create": "Create new user",
"download_report": "Download report",
"empty": "Couldn't find any users",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@
"select_country": "Seleccionar un país",
"select_terms": "Seleccionar una versión de términos",
"select_society": "Seleccionar una sociedad nacional",
"search": "Buscar",
"search_placeholder": "Nombre o correo (mín. 3 caracteres)",
"create": "Crear nuevo usuario",
"download_report": "Descargar informe",
"empty": "No se encontraron usuarios",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@
"select_country": "Sélectionner un pays",
"select_terms": "Sélectionner une version des conditions",
"select_society": "Sélectionner une société nationale",
"search": "Rechercher",
"search_placeholder": "Nom ou e-mail (3 caractères min.)",
"create": "Créer un nouvel utilisateur",
"download_report": "Télécharger le rapport",
"empty": "Aucun utilisateur trouvé",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/ht.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "Chwazi yon peyi",
"select_terms": "Chwazi yon vèsyon tèm yo",
"select_society": "Chwazi yon sosyete nasyonal",
"search": "Chèche",
"search_placeholder": "Non oswa imel (omwen 3 karaktè)",
"create": "Kreye nouvo itilizatè",
"download_report": "Telechaje rapò",
"empty": "Yo pa jwenn itilizatè yo",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "Select a country",
"select_terms": "Select a terms version",
"select_society": "Select a nacional society",
"search": "Cari",
"search_placeholder": "Nama atau email (min. 3 karakter)",
"create": "Membuat pengguna baru",
"download_report": "Unduh laporan",
"empty": "Tidak dapat menemukan pengguna",
Expand Down
2 changes: 2 additions & 0 deletions resources/lang/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@
"select_country": "Seleziona un paese",
"select_terms": "Seleziona una versione dei termini",
"select_society": "Seleziona una società nazionale",
"search": "Cerca",
"search_placeholder": "Nome o email (min. 3 caratteri)",
"create": "Crea nuovo utente",
"download_report": "Scarica il report",
"empty": "Non siamo riusciti a trovare alcun utente",
Expand Down
Loading
Loading