diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -76,12 +76,56 @@
public function index()
{
$user = $this->guard()->user();
+ $search = trim(request()->input('search'));
+ $page = intval(request()->input('page')) ?: 1;
+ $pageSize = 20;
+ $hasMore = false;
+ $result = collect([]);
+
+ $result = $user->users();
+
+ // Search by user email, alias or name
+ if (strlen($search) > 0) {
+ // thanks to cloning we skip some extra queries in $user->users()
+ $allUsers1 = clone $result;
+ $allUsers2 = clone $result;
+
+ $result->whereLike('email', $search)
+ ->union(
+ $allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id')
+ ->whereLike('alias', $search)
+ )
+ ->union(
+ $allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id')
+ ->whereLike('value', $search)
+ ->whereIn('key', ['first_name', 'last_name'])
+ );
+ }
+
+ $result = $result->orderBy('email')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ // Process the result
+ $result = $result->map(
+ function ($user) {
+ $data = $user->toArray();
+ $data = array_merge($data, self::userStatuses($user));
+ return $data;
+ }
+ );
- $result = $user->users()->orderBy('email')->get()->map(function ($user) {
- $data = $user->toArray();
- $data = array_merge($data, self::userStatuses($user));
- return $data;
- });
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ ];
return response()->json($result);
}
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -382,6 +382,7 @@
'reset-2fa' => "Reset 2-Factor Auth",
'reset-2fa-title' => "2-Factor Authentication Reset",
'title' => "User account",
+ 'search' => "User email address or name",
'search-pl' => "User ID, email or domain",
'skureq' => "{sku} requires {list}.",
'subscription' => "Subscription",
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -4,12 +4,20 @@
{{ $t('user.list-title') }}
-
- {{ $t('user.create') }}
-
-
+
+
+
+
+ {{ $t('user.create') }}
+
+
+
+
{{ $t('form.primary-email') }} |
@@ -29,6 +37,9 @@
+
+
+
@@ -39,19 +50,69 @@
export default {
data() {
return {
- users: [],
- current_user: null
+ hasMore: false,
+ page: 1,
+ search: '',
+ users: []
}
},
- created() {
+ mounted() {
this.$root.startLoading()
+ this.loadUsers(null, () => this.$root.stopLoading())
+ },
+ methods: {
+ loadUsers(params, callback) {
+ let loader
+ let get = {}
+
+ if (params) {
+ if (params.reset) {
+ this.users = []
+ this.page = 0
+ }
+
+ get.page = params.page || (this.page + 1)
+
+ if (typeof params === 'object' && 'search' in params) {
+ get.search = params.search
+ this.currentSearch = params.search
+ } else {
+ get.search = this.currentSearch
+ }
+
+ loader = $(get.page > 1 ? '#more-loader' : '#users-list tfoot td')
+ } else {
+ this.currentSearch = null
+ }
- axios.get('/api/v4/users')
- .then(response => {
- this.$root.stopLoading()
- this.users = response.data
- })
- .catch(this.$root.errorHandler)
+ this.$root.addLoader(loader)
+
+ axios.get('/api/v4/users', { params: get })
+ .then(response => {
+ this.$root.removeLoader(loader)
+
+ // Note: In Vue we can't just use .concat()
+ for (let i in response.data.list) {
+ this.$set(this.users, this.users.length, response.data.list[i])
+ }
+ this.hasMore = response.data.hasMore
+ this.page = response.data.page || 1
+
+ if (callback) {
+ callback()
+ }
+ })
+ .catch(error => {
+ this.$root.removeLoader(loader)
+
+ if (callback) {
+ callback()
+ }
+ })
+ },
+ searchUsers() {
+ this.loadUsers({ reset: true, search: this.search })
+ }
}
}
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -229,9 +229,12 @@
*/
public function vueClear($selector)
{
+/*
if ($this->resolver->prefix != 'body') {
$selector = $this->resolver->prefix . ' ' . $selector;
}
+*/
+ $selector = $this->resolver->format($selector);
// The existing clear(), and type() with empty string do not work.
// We have to clear the field and dispatch 'input' event programatically.
diff --git a/src/tests/Browser/Pages/UserList.php b/src/tests/Browser/Pages/UserList.php
--- a/src/tests/Browser/Pages/UserList.php
+++ b/src/tests/Browser/Pages/UserList.php
@@ -39,6 +39,7 @@
{
return [
'@app' => '#app',
+ '@search' => '#search-form',
'@table' => '#user-list table',
];
}
diff --git a/src/tests/Browser/UserListTest.php b/src/tests/Browser/UserListTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/UserListTest.php
@@ -0,0 +1,99 @@
+browse(function (Browser $browser) {
+ $browser->visit('/users')->on(new Home());
+ });
+ }
+
+ /**
+ * Test users list page
+ */
+ public function testList(): void
+ {
+ $this->browse(function (Browser $browser) {
+ // Test that the page requires authentication
+ // Test the list
+ $browser->visit(new Home())
+ ->submitLogon('john@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->assertSeeIn('@links .link-users', 'User accounts')
+ ->click('@links .link-users')
+ ->on(new UserList())
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')
+ ->assertElementsCount('tbody tr', 4)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
+ ->assertMissing('tfoot');
+ });
+
+ // Test searching
+ $browser->assertValue('@search input', '')
+ ->assertAttribute('@search input', 'placeholder', 'User email address or name')
+ ->assertSeeIn('@search button', 'Search')
+ ->type('@search input', 'jo')
+ ->click('@search button')
+ ->waitUntilMissing('@app .app-loader')
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')
+ ->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) a', 'joe@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) a', 'john@kolab.org')
+ ->assertMissing('tfoot');
+ })
+ // reset search
+ ->vueClear('@search input')
+ ->keys('@search input', '{enter}')
+ ->waitUntilMissing('@app .app-loader')
+ ->whenAvailable('@table', function (Browser $browser) {
+ $browser->waitFor('tbody tr')->assertElementsCount('tbody tr', 4);
+ });
+
+ // TODO: Test paging
+
+ $browser->click('@table tr:nth-child(3)')
+ ->on(new UserInfo())
+ ->assertSeeIn('#user-info .card-title', 'User account')
+ ->with('@general', function (Browser $browser) {
+ $browser->assertValue('#email', 'john@kolab.org');
+ });
+ });
+ }
+}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -87,65 +87,19 @@
parent::tearDown();
}
- /**
- * Test user info page (unauthenticated)
- */
- public function testInfoUnauth(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $user = User::where('email', 'john@kolab.org')->first();
-
- $browser->visit('/user/' . $user->id)->on(new Home());
- });
- }
-
- /**
- * Test users list page (unauthenticated)
- */
- public function testListUnauth(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $browser->visit('/users')->on(new Home());
- });
- }
-
- /**
- * Test users list page
- */
- public function testList(): void
- {
- // Test that the page requires authentication
- $this->browse(function (Browser $browser) {
- $browser->visit(new Home())
- ->submitLogon('john@kolab.org', 'simple123', true)
- ->on(new Dashboard())
- ->assertSeeIn('@links .link-users', 'User accounts')
- ->click('@links .link-users')
- ->on(new UserList())
- ->whenAvailable('@table', function (Browser $browser) {
- $browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 4)
- ->assertSeeIn('tbody tr:nth-child(1) a', 'jack@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(2) a', 'joe@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(3) a', 'john@kolab.org')
- ->assertSeeIn('tbody tr:nth-child(4) a', 'ned@kolab.org')
- ->assertMissing('tfoot');
- });
- });
- }
-
/**
* Test user account editing page (not profile page)
- *
- * @depends testList
*/
public function testInfo(): void
{
$this->browse(function (Browser $browser) {
- $browser->on(new UserList())
- ->click('@table tr:nth-child(3) a')
+ $user = User::where('email', 'john@kolab.org')->first();
+
+ // Test that the page requires authentication
+ $browser->visit('/user/' . $user->id)
+ ->on(new Home())
+ ->screenshot('aa')
+ ->submitLogon('john@kolab.org', 'simple123', false)
->on(new UserInfo())
->assertSeeIn('#user-info .card-title', 'User account')
->with('@general', function (Browser $browser) {
@@ -366,7 +320,7 @@
/**
* Test user adding page
*
- * @depends testList
+ * @depends testInfo
*/
public function testNewUser(): void
{
@@ -746,7 +700,7 @@
/**
* Test beta entitlements
*
- * @depends testList
+ * @depends testInfo
*/
public function testBetaEntitlements(): void
{
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -196,35 +196,76 @@
$json = $response->json();
- $this->assertCount(0, $json);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(0, $json['count']);
+ $this->assertCount(0, $json['list']);
$response = $this->actingAs($john)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(4, $json);
- $this->assertSame($jack->email, $json[0]['email']);
- $this->assertSame($joe->email, $json[1]['email']);
- $this->assertSame($john->email, $json[2]['email']);
- $this->assertSame($ned->email, $json[3]['email']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(4, $json['count']);
+ $this->assertCount(4, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+ $this->assertSame($joe->email, $json['list'][1]['email']);
+ $this->assertSame($john->email, $json['list'][2]['email']);
+ $this->assertSame($ned->email, $json['list'][3]['email']);
// Values below are tested by Unit tests
- $this->assertArrayHasKey('isDeleted', $json[0]);
- $this->assertArrayHasKey('isSuspended', $json[0]);
- $this->assertArrayHasKey('isActive', $json[0]);
- $this->assertArrayHasKey('isLdapReady', $json[0]);
- $this->assertArrayHasKey('isImapReady', $json[0]);
+ $this->assertArrayHasKey('isDeleted', $json['list'][0]);
+ $this->assertArrayHasKey('isSuspended', $json['list'][0]);
+ $this->assertArrayHasKey('isActive', $json['list'][0]);
+ $this->assertArrayHasKey('isLdapReady', $json['list'][0]);
+ $this->assertArrayHasKey('isImapReady', $json['list'][0]);
$response = $this->actingAs($ned)->get("/api/v4/users");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(4, $json);
- $this->assertSame($jack->email, $json[0]['email']);
- $this->assertSame($joe->email, $json[1]['email']);
- $this->assertSame($john->email, $json[2]['email']);
- $this->assertSame($ned->email, $json[3]['email']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(4, $json['count']);
+ $this->assertCount(4, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+ $this->assertSame($joe->email, $json['list'][1]['email']);
+ $this->assertSame($john->email, $json['list'][2]['email']);
+ $this->assertSame($ned->email, $json['list'][3]['email']);
+
+ // Search by user email
+ $response = $this->actingAs($john)->get("/api/v4/users?search=jack@k");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($jack->email, $json['list'][0]['email']);
+
+ // Search by alias
+ $response = $this->actingAs($john)->get("/api/v4/users?search=monster");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($joe->email, $json['list'][0]['email']);
+
+ // Search by name
+ $response = $this->actingAs($john)->get("/api/v4/users?search=land");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($ned->email, $json['list'][0]['email']);
+
+ // TODO: Test paging
}
/**