Explorar o código

Updated aliases table view

Will Browning %!s(int64=5) %!d(string=hai) anos
pai
achega
aaf08d3961
Modificáronse 7 ficheiros con 640 adicións e 455 borrados
  1. 4 4
      app/Console/Commands/ReceiveEmail.php
  2. 95 97
      composer.lock
  3. 46 0
      package-lock.json
  4. 1 0
      package.json
  5. 270 0
      resources/css/app.css
  6. 2 0
      resources/js/app.js
  7. 222 354
      resources/js/pages/Aliases.vue

+ 4 - 4
app/Console/Commands/ReceiveEmail.php

@@ -112,7 +112,7 @@ class ReceiveEmail extends Command
                 }
 
                 // If there is still no user or the user has no verified default recipient then continue.
-                if (is_null($user) || !$user->hasVerifiedDefaultRecipient()) {
+                if (!isset($user) || !$user->hasVerifiedDefaultRecipient()) {
                     continue;
                 }
 
@@ -152,7 +152,7 @@ class ReceiveEmail extends Command
         if (!is_null($alias) && filter_var($displayTo, FILTER_VALIDATE_EMAIL)) {
             $emailData = new EmailData($this->parser);
 
-            $message = (new ReplyToEmail($user, $alias, $emailData));
+            $message = new ReplyToEmail($user, $alias, $emailData);
 
             Mail::to($displayTo)->queue($message);
 
@@ -192,7 +192,7 @@ class ReceiveEmail extends Command
                                     ->oldest()
                                     ->get()
                                     ->filter(function ($item, $key) use ($keys) {
-                                        return in_array($key+1, $keys) && ! is_null($item['email_verified_at']);
+                                        return in_array($key+1, $keys) && !is_null($item['email_verified_at']);
                                     })
                                     ->pluck('id')
                                     ->take(10)
@@ -210,7 +210,7 @@ class ReceiveEmail extends Command
         $emailData = new EmailData($this->parser);
 
         $alias->verifiedRecipientsOrDefault()->each(function ($recipient) use ($alias, $emailData) {
-            $message = (new ForwardEmail($alias, $emailData, $recipient->should_encrypt ? $recipient->fingerprint : null));
+            $message = new ForwardEmail($alias, $emailData, $recipient->should_encrypt ? $recipient->fingerprint : null);
 
             Mail::to($recipient->email)->queue($message);
         });

+ 95 - 97
composer.lock

@@ -1659,16 +1659,16 @@
         },
         {
             "name": "php-mime-mail-parser/php-mime-mail-parser",
-            "version": "5.0.3",
+            "version": "5.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-mime-mail-parser/php-mime-mail-parser.git",
-                "reference": "a58f17fe63ea35a2b8a7fadb5cb8e123bd0787b4"
+                "reference": "7d4be053748c2e8a1f64781a648be3cc397351fe"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/a58f17fe63ea35a2b8a7fadb5cb8e123bd0787b4",
-                "reference": "a58f17fe63ea35a2b8a7fadb5cb8e123bd0787b4",
+                "url": "https://api.github.com/repos/php-mime-mail-parser/php-mime-mail-parser/zipball/7d4be053748c2e8a1f64781a648be3cc397351fe",
+                "reference": "7d4be053748c2e8a1f64781a648be3cc397351fe",
                 "shasum": ""
             },
             "require": {
@@ -1698,33 +1698,33 @@
             "authors": [
                 {
                     "name": "eXorus",
-                    "role": "Developer",
                     "email": "exorus.spam@gmail.com",
-                    "homepage": "https://github.com/eXorus/"
+                    "homepage": "https://github.com/eXorus/",
+                    "role": "Developer"
                 },
                 {
                     "name": "M.Valinskis",
-                    "role": "Developer",
                     "email": "M.Valins@gmail.com",
-                    "homepage": "https://code.google.com/p/php-mime-mail-parser"
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
                 },
                 {
                     "name": "eugene.emmett.wood",
-                    "role": "Developer",
                     "email": "gene_w@cementhorizon.com",
-                    "homepage": "https://code.google.com/p/php-mime-mail-parser"
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
                 },
                 {
                     "name": "alknetso",
-                    "role": "Developer",
                     "email": "alkne@gmail.com",
-                    "homepage": "https://code.google.com/p/php-mime-mail-parser"
+                    "homepage": "https://code.google.com/p/php-mime-mail-parser",
+                    "role": "Developer"
                 },
                 {
                     "name": "bucabay",
-                    "role": "Developer",
                     "email": "gabe@fijiwebdesign.com",
-                    "homepage": "http://www.fijiwebdesign.com"
+                    "homepage": "http://www.fijiwebdesign.com",
+                    "role": "Developer"
                 }
             ],
             "description": "A fully tested email parser for PHP 7.1+ (mailparse extension wrapper).",
@@ -1737,7 +1737,7 @@
                 "parser",
                 "php"
             ],
-            "time": "2019-08-14T09:28:59+00:00"
+            "time": "2019-09-09T11:42:01+00:00"
         },
         {
             "name": "phpoption/phpoption",
@@ -1791,16 +1791,16 @@
         },
         {
             "name": "pragmarx/google2fa",
-            "version": "v5.0.0",
+            "version": "v6.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/antonioribeiro/google2fa.git",
-                "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4"
+                "reference": "03f6fb65aaccc21d6f70969db652316ad003b83d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/17c969c82f427dd916afe4be50bafc6299aef1b4",
-                "reference": "17c969c82f427dd916afe4be50bafc6299aef1b4",
+                "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/03f6fb65aaccc21d6f70969db652316ad003b83d",
+                "reference": "03f6fb65aaccc21d6f70969db652316ad003b83d",
                 "shasum": ""
             },
             "require": {
@@ -1810,7 +1810,7 @@
                 "symfony/polyfill-php56": "~1.2"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4|~5|~6"
+                "phpunit/phpunit": "~4|~5|~6|~7|~8"
             },
             "type": "library",
             "extra": {
@@ -1832,8 +1832,8 @@
             "authors": [
                 {
                     "name": "Antonio Carlos Ribeiro",
-                    "role": "Creator & Designer",
-                    "email": "acr@antoniocarlosribeiro.com"
+                    "email": "acr@antoniocarlosribeiro.com",
+                    "role": "Creator & Designer"
                 }
             ],
             "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
@@ -1843,20 +1843,20 @@
                 "Two Factor Authentication",
                 "google2fa"
             ],
-            "time": "2019-03-19T22:44:16+00:00"
+            "time": "2019-09-11T19:19:55+00:00"
         },
         {
             "name": "pragmarx/google2fa-laravel",
-            "version": "v1.0.1",
+            "version": "v1.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/antonioribeiro/google2fa-laravel.git",
-                "reference": "b5f5bc71dcc52c48720441bc01c701023bd82882"
+                "reference": "6d9787e0311879965c15d743be5022795af19653"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/b5f5bc71dcc52c48720441bc01c701023bd82882",
-                "reference": "b5f5bc71dcc52c48720441bc01c701023bd82882",
+                "url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/6d9787e0311879965c15d743be5022795af19653",
+                "reference": "6d9787e0311879965c15d743be5022795af19653",
                 "shasum": ""
             },
             "require": {
@@ -1865,8 +1865,8 @@
                 "pragmarx/google2fa-qrcode": "^1.0"
             },
             "require-dev": {
-                "orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*",
-                "phpunit/phpunit": "~5|~6|~7"
+                "orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*|4.*",
+                "phpunit/phpunit": "~5|~6|~7|~8"
             },
             "suggest": {
                 "bacon/bacon-qr-code": "Required to generate inline QR Codes.",
@@ -1903,8 +1903,8 @@
             "authors": [
                 {
                     "name": "Antonio Carlos Ribeiro",
-                    "role": "Creator & Designer",
-                    "email": "acr@antoniocarlosribeiro.com"
+                    "email": "acr@antoniocarlosribeiro.com",
+                    "role": "Creator & Designer"
                 }
             ],
             "description": "A One Time Password Authentication package, compatible with Google Authenticator.",
@@ -1914,7 +1914,7 @@
                 "google2fa",
                 "laravel"
             ],
-            "time": "2019-03-22T19:54:51+00:00"
+            "time": "2019-09-13T22:23:38+00:00"
         },
         {
             "name": "pragmarx/google2fa-qrcode",
@@ -3996,16 +3996,16 @@
         },
         {
             "name": "vlucas/phpdotenv",
-            "version": "v3.5.0",
+            "version": "v3.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/vlucas/phpdotenv.git",
-                "reference": "95cb0fa6c025f7f0db7fc60f81e9fb231eb2d222"
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/95cb0fa6c025f7f0db7fc60f81e9fb231eb2d222",
-                "reference": "95cb0fa6c025f7f0db7fc60f81e9fb231eb2d222",
+                "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1bdf24f065975594f6a117f0f1f6cabf1333b156",
+                "reference": "1bdf24f065975594f6a117f0f1f6cabf1333b156",
                 "shasum": ""
             },
             "require": {
@@ -4014,12 +4014,12 @@
                 "symfony/polyfill-ctype": "^1.9"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0"
+                "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "3.5-dev"
+                    "dev-master": "3.6-dev"
                 }
             },
             "autoload": {
@@ -4049,7 +4049,7 @@
                 "env",
                 "environment"
             ],
-            "time": "2019-08-27T17:00:38+00:00"
+            "time": "2019-09-10T21:37:39+00:00"
         }
     ],
     "packages-dev": [
@@ -4924,35 +4924,33 @@
         },
         {
             "name": "phpdocumentor/reflection-common",
-            "version": "1.0.1",
+            "version": "2.0.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
-                "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+                "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5"
+                "php": ">=7.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.6"
+                "phpunit/phpunit": "~6"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -4974,30 +4972,30 @@
                 "reflection",
                 "static analysis"
             ],
-            "time": "2017-09-11T18:02:19+00:00"
+            "time": "2018-08-07T13:53:10+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
-            "version": "4.3.1",
+            "version": "4.3.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
-                "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c"
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
-                "reference": "bdd9f737ebc2a01c06ea7ff4308ec6697db9b53c",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+                "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
-                "phpdocumentor/reflection-common": "^1.0.0",
-                "phpdocumentor/type-resolver": "^0.4.0",
+                "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+                "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
                 "webmozart/assert": "^1.0"
             },
             "require-dev": {
-                "doctrine/instantiator": "~1.0.5",
+                "doctrine/instantiator": "^1.0.5",
                 "mockery/mockery": "^1.0",
                 "phpunit/phpunit": "^6.4"
             },
@@ -5025,41 +5023,40 @@
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2019-04-30T17:48:53+00:00"
+            "time": "2019-09-12T14:27:41+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
-            "version": "0.4.0",
+            "version": "1.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpDocumentor/TypeResolver.git",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
-                "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+                "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
                 "shasum": ""
             },
             "require": {
-                "php": "^5.5 || ^7.0",
-                "phpdocumentor/reflection-common": "^1.0"
+                "php": "^7.1",
+                "phpdocumentor/reflection-common": "^2.0"
             },
             "require-dev": {
-                "mockery/mockery": "^0.9.4",
-                "phpunit/phpunit": "^5.2||^4.8.24"
+                "ext-tokenizer": "^7.1",
+                "mockery/mockery": "~1",
+                "phpunit/phpunit": "^7.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "1.x-dev"
                 }
             },
             "autoload": {
                 "psr-4": {
-                    "phpDocumentor\\Reflection\\": [
-                        "src/"
-                    ]
+                    "phpDocumentor\\Reflection\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -5072,7 +5069,8 @@
                     "email": "me@mikevanriel.com"
                 }
             ],
-            "time": "2017-07-14T14:27:02+00:00"
+            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+            "time": "2019-08-22T18:11:29+00:00"
         },
         {
             "name": "phpspec/prophecy",
@@ -5139,16 +5137,16 @@
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "7.0.7",
+            "version": "7.0.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800"
+                "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
-                "reference": "7743bbcfff2a907e9ee4a25be13d0f8ec5e73800",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
+                "reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
                 "shasum": ""
             },
             "require": {
@@ -5157,7 +5155,7 @@
                 "php": "^7.2",
                 "phpunit/php-file-iterator": "^2.0.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^3.1.0",
+                "phpunit/php-token-stream": "^3.1.1",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
                 "sebastian/environment": "^4.2.2",
                 "sebastian/version": "^2.0.1",
@@ -5187,8 +5185,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sebastian@phpunit.de"
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
                 }
             ],
             "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
@@ -5198,7 +5196,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2019-07-25T05:31:54+00:00"
+            "time": "2019-09-17T06:24:36+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
@@ -5342,16 +5340,16 @@
         },
         {
             "name": "phpunit/php-token-stream",
-            "version": "3.1.0",
+            "version": "3.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-token-stream.git",
-                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a"
+                "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e899757bb3df5ff6e95089132f32cd59aac2220a",
-                "reference": "e899757bb3df5ff6e95089132f32cd59aac2220a",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
+                "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
                 "shasum": ""
             },
             "require": {
@@ -5387,20 +5385,20 @@
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2019-07-25T05:29:42+00:00"
+            "time": "2019-09-17T06:23:10+00:00"
         },
         {
             "name": "phpunit/phpunit",
-            "version": "8.3.4",
+            "version": "8.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "e31cce0cf4499c0ccdbbb211a3280d36ab341e36"
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e31cce0cf4499c0ccdbbb211a3280d36ab341e36",
-                "reference": "e31cce0cf4499c0ccdbbb211a3280d36ab341e36",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/302faed7059fde575cf3403a78c730c5e3a62750",
+                "reference": "302faed7059fde575cf3403a78c730c5e3a62750",
                 "shasum": ""
             },
             "require": {
@@ -5423,7 +5421,7 @@
                 "sebastian/comparator": "^3.0.2",
                 "sebastian/diff": "^3.0.2",
                 "sebastian/environment": "^4.2.2",
-                "sebastian/exporter": "^3.1.0",
+                "sebastian/exporter": "^3.1.1",
                 "sebastian/global-state": "^3.0.0",
                 "sebastian/object-enumerator": "^3.0.3",
                 "sebastian/resource-operations": "^2.0.1",
@@ -5459,8 +5457,8 @@
             "authors": [
                 {
                     "name": "Sebastian Bergmann",
-                    "role": "lead",
-                    "email": "sebastian@phpunit.de"
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
                 }
             ],
             "description": "The PHP Unit Testing framework.",
@@ -5470,7 +5468,7 @@
                 "testing",
                 "xunit"
             ],
-            "time": "2019-08-11T06:56:55+00:00"
+            "time": "2019-09-14T09:12:03+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
@@ -5692,16 +5690,16 @@
         },
         {
             "name": "sebastian/exporter",
-            "version": "3.1.1",
+            "version": "3.1.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/exporter.git",
-                "reference": "06a9a5947f47b3029d76118eb5c22802e5869687"
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/06a9a5947f47b3029d76118eb5c22802e5869687",
-                "reference": "06a9a5947f47b3029d76118eb5c22802e5869687",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+                "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
                 "shasum": ""
             },
             "require": {
@@ -5755,7 +5753,7 @@
                 "export",
                 "exporter"
             ],
-            "time": "2019-08-11T12:43:14+00:00"
+            "time": "2019-09-14T09:02:43+00:00"
         },
         {
             "name": "sebastian/global-state",

+ 46 - 0
package-lock.json

@@ -2688,6 +2688,11 @@
             "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz",
             "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw=="
         },
+        "diacriticless": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/diacriticless/-/diacriticless-1.0.1.tgz",
+            "integrity": "sha1-592peMKRlgm7SK7h78XeajN71MM="
+        },
         "diffie-hellman": {
             "version": "5.0.3",
             "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -5371,11 +5376,26 @@
             "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
             "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
         },
+        "lodash.clonedeep": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+            "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+        },
         "lodash.defaults": {
             "version": "4.2.0",
             "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
             "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
         },
+        "lodash.filter": {
+            "version": "4.6.0",
+            "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
+            "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4="
+        },
+        "lodash.foreach": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+            "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM="
+        },
         "lodash.isarguments": {
             "version": "3.1.0",
             "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -5386,6 +5406,11 @@
             "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
             "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
         },
+        "lodash.isequal": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+            "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+        },
         "lodash.keys": {
             "version": "3.1.2",
             "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
@@ -9133,6 +9158,27 @@
             "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
             "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
         },
+        "vue-good-table": {
+            "version": "2.18.0",
+            "resolved": "https://registry.npmjs.org/vue-good-table/-/vue-good-table-2.18.0.tgz",
+            "integrity": "sha512-wC0I9D7TyfSf0zrYwIpNtXdbSPRir//q7TNpGZ8/MfgW5mLMspk9I0MVq856M3ecgwTqgqXXtO87+i0sgQxALg==",
+            "requires": {
+                "date-fns": "^2.0.0-beta.4",
+                "diacriticless": "1.0.1",
+                "lodash.assign": "^4.2.0",
+                "lodash.clonedeep": "^4.5.0",
+                "lodash.filter": "^4.6.0",
+                "lodash.foreach": "^4.5.0",
+                "lodash.isequal": "^4.5.0"
+            },
+            "dependencies": {
+                "date-fns": {
+                    "version": "2.2.1",
+                    "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.2.1.tgz",
+                    "integrity": "sha512-4V1i5CnTinjBvJpXTq7sDHD4NY6JPcl15112IeSNNLUWQOQ+kIuCvRGOFZMQZNvkadw8F9QTyZxz59rIRU6K+w=="
+                }
+            }
+        },
         "vue-hot-reload-api": {
             "version": "2.3.3",
             "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
         "tippy.js": "^4.3.5",
         "v-clipboard": "^2.2.2",
         "vue": "^2.6.10",
+        "vue-good-table": "^2.18.0",
         "vue-multiselect": "^2.1.6",
         "vue-notification": "^1.3.16",
         "vue-template-compiler": "^2.6.10"

+ 270 - 0
resources/css/app.css

@@ -43,6 +43,276 @@ html {
 .multiselect .multiselect__option--selected.multiselect__option--highlight {
   @apply bg-red-500;
 }
+
+/* Vue good-table */
+.vgt-right-align {
+  @apply text-left;
+}
+
+.vgt-left-align {
+  @apply text-left;
+}
+
+.vgt-center-align {
+  @apply text-center;
+}
+
+.vgt-pull-left {
+  @apply float-left !important;
+}
+
+.vgt-pull-right {
+  @apply float-right !important;
+}
+
+.vgt-clearfix::after {
+  @apply block;
+  content: '';
+  clear: both;
+}
+
+.vgt-responsive {
+  @apply w-full overflow-x-auto relative;
+}
+
+.vgt-text-disabled {
+  @apply text-grey-300;
+}
+
+.vgt-wrap {
+  @apply shadow relative;
+}
+
+.vgt-fixed-header {
+  @apply absolute z-10 w-full overflow-x-auto;
+}
+
+table.vgt-table {
+  @apply text-base border-collapse bg-white w-full max-w-full table-auto;
+}
+
+table.vgt-table td {
+  @apply p-3 border-b border-grey-100;
+}
+
+table.vgt-table tr.clickable {
+  @apply cursor-pointer;
+}
+
+table.vgt-table tr.clickable:hover {
+  @apply bg-grey-50;
+}
+
+.vgt-table th {
+  @apply p-3 align-middle relative;
+}
+
+.vgt-table th.sorting {
+  @apply cursor-pointer;
+}
+
+.vgt-table th.sorting:after {
+  @apply hidden absolute h-0 w-0;
+  content: '';
+  right: 6px;
+  top: 50%;
+  margin-top: -3px;
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid theme('colors.cyan.500');
+}
+
+.vgt-table th.sorting:hover:after {
+  @apply inline-block;
+  border-bottom-color: theme('colors.cyan.400');
+}
+
+.vgt-table th.line-numbers,
+.vgt-table th.vgt-checkbox-col {
+  @apply py-0 pr-1 pl-3 break-words w-6 text-center bg-white border border-t-0 border-r-0 border-l-0 border-grey-100;
+}
+
+.vgt-table th.vgt-checkbox-col input {
+  @apply appearance-none inline-block align-middle flex-shrink-0 h-4 w-4 bg-white rounded border border-grey-100 text-cyan-500;
+  color-adjust: exact;
+  background-origin: border-box;
+  user-select: none;
+}
+
+.vgt-table th.vgt-checkbox-col input:checked {
+  background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
+  border-color: transparent;
+  background-color: currentColor;
+  background-size: 100% 100%;
+  background-position: center;
+  background-repeat: no-repeat;
+}
+
+.vgt-table th.filter-th {
+  @apply p-3;
+}
+
+.vgt-table th.vgt-row-header {
+  @apply border-t-2 border-b-2 border-grey-100 bg-white;
+}
+
+.vgt-table thead th {
+  @apply text-grey-400 align-bottom pr-6 bg-white cursor-pointer;
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.vgt-table thead th.vgt-checkbox-col {
+  @apply align-middle;
+}
+
+.vgt-table thead th.sorting-asc,
+.vgt-table thead th.sorting-desc {
+  @apply text-grey-800;
+}
+
+.vgt-table thead th.sorting-asc:after,
+.vgt-table thead th.sorting-desc:after {
+  @apply block;
+  content: '';
+}
+
+.vgt-table thead th.sorting-desc:after {
+  border-top: 6px solid theme('colors.cyan.500');
+  border-left: 6px solid transparent;
+  border-right: 6px solid transparent;
+  border-bottom: none;
+}
+
+.vgt-input,
+.vgt-select {
+  @apply w-full leading-none block text-sm font-normal text-grey-400 bg-white border;
+  height: 40px;
+  padding: 6px 12px;
+  border-radius: 4px;
+  box-sizing: border-box;
+  background-image: none;
+  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+.vgt-input::placeholder,
+.vgt-select::placeholder {
+  @apply text-grey-500 opacity-25;
+}
+
+.vgt-input:focus,
+.vgt-select:focus {
+  @apply outline-none border-cyan-500;
+}
+
+.vgt-selection-info-row {
+  @apply text-yellow-600 bg-yellow-50 p-3 text-sm font-bold;
+}
+
+.vgt-selection-info-row a {
+  @apply font-bold inline-block ml-2;
+}
+
+.vgt-wrap__actions-footer {
+  @apply border border-grey-100;
+}
+
+.vgt-wrap__footer {
+  @apply p-4 text-grey-400 bg-white border border-grey-200;
+}
+
+.vgt-wrap__footer .footer__row-count__label,
+.vgt-wrap__footer .footer__row-count__select {
+  @apply inline-block align-middle;
+}
+
+.vgt-wrap__footer .footer__row-count__label {
+  @apply text-sm text-grey-500;
+}
+
+.vgt-wrap__footer .footer__row-count__select {
+  @apply w-auto p-0 border-0 border-r-0 h-auto text-sm ml-2 text-grey-500 font-bold;
+}
+
+.vgt-wrap__footer .footer__row-count__select:focus {
+  @apply outline-none border-cyan-500;
+}
+
+.vgt-wrap__footer .footer__navigation {
+  @apply text-sm;
+}
+
+.vgt-wrap__footer .footer__navigation__info,
+.vgt-wrap__footer .footer__navigation__page-btn,
+.vgt-wrap__footer .footer__navigation__page-info {
+  @apply inline-block align-middle;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn {
+  @apply no-underline text-grey-500 font-bold whitespace-no-wrap;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn:focus {
+  @apply outline-none border-0;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn.disabled,
+.vgt-wrap__footer .footer__navigation__page-btn.disabled:hover {
+  @apply opacity-50 cursor-not-allowed;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn.disabled .chevron.left:after,
+.vgt-wrap__footer .footer__navigation__page-btn.disabled:hover .chevron.left:after {
+  border-right-color: #606266;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn.disabled .chevron.right:after,
+.vgt-wrap__footer .footer__navigation__page-btn.disabled:hover .chevron.right:after {
+  border-left-color: #606266;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn .chevron {
+  @apply h-6 w-6 relative my-0 mx-2;
+  border-radius: 15%;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn .chevron:after {
+  @apply absolute block;
+  content: '';
+  left: 50%;
+  top: 50%;
+  margin-top: -6px;
+  border-top: 6px solid transparent;
+  border-bottom: 6px solid transparent;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn .chevron.left::after {
+  border-right: 6px solid theme('colors.cyan.500');
+  margin-left: -3px;
+}
+
+.vgt-wrap__footer .footer__navigation__page-btn .chevron.right::after {
+  border-left: 6px solid theme('colors.cyan.500');
+  margin-left: -3px;
+}
+
+.vgt-wrap__footer .footer__navigation__info,
+.vgt-wrap__footer .footer__navigation__page-info {
+  @apply inline-block text-grey-600 my-0 mx-4;
+}
+
+.vgt-wrap__footer .footer__navigation__page-info__current-entry {
+  @apply text-center w-6 inline-block my-0 mx-2 font-bold;
+}
+
+@media only screen and (max-width: 750px) {
+  .vgt-wrap__footer .footer__navigation__info {
+    @apply hidden;
+  }
+
+  .vgt-wrap__footer .footer__navigation__page-btn {
+    @apply ml-4;
+  }
+}
 /* purgecss end ignore */
 
 @tailwind utilities;

+ 2 - 0
resources/js/app.js

@@ -14,10 +14,12 @@ window.Vue = require('vue')
 import PortalVue from 'portal-vue'
 import Clipboard from 'v-clipboard'
 import Notifications from 'vue-notification'
+import VueGoodTablePlugin from 'vue-good-table'
 
 Vue.use(PortalVue)
 Vue.use(Clipboard)
 Vue.use(Notifications)
+Vue.use(VueGoodTablePlugin)
 
 Vue.component('loader', require('./components/Loader.vue').default)
 Vue.component('dropdown', require('./components/DropdownNav.vue').default)

+ 222 - 354
resources/js/pages/Aliases.vue

@@ -127,330 +127,153 @@
         </button>
       </div>
     </div>
-    <div class="bg-white rounded shadow overflow-x-auto">
-      <table v-if="initialAliases.length" class="w-full whitespace-no-wrap">
-        <tr class="text-left font-semibold text-grey-500 text-sm tracking-wider">
-          <th class="pl-4 pr-2 py-4">
-            <div class="flex items-center">
-              Created
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('created_at', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('created_at', 'asc') }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('created_at', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('created_at', 'desc'),
-                  }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4">
-            <div class="flex items-center">
-              Alias
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('email', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('email', 'asc') }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('email', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('email', 'desc') }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4">
-            <div class="flex items-center">
-              Recipients
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('recipients', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('recipients', 'asc') }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('recipients', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('recipients', 'desc') }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4">
-            <div class="flex items-center">
-              Description
-            </div>
-          </th>
-          <th class="px-2 py-4">
-            <div class="flex items-center">
-              Forwarded
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('emails_forwarded', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_forwarded', 'asc'),
-                  }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('emails_forwarded', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_forwarded', 'desc'),
-                  }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4 items-center">
-            <div class="flex items-center">
-              Blocked
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('emails_blocked', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_blocked', 'asc'),
-                  }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('emails_blocked', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_blocked', 'desc'),
-                  }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4 items-center">
-            <div class="flex items-center">
-              Replies
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('emails_replied', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_replied', 'asc'),
-                  }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('emails_replied', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{
-                    'text-grey-800': isCurrentSort('emails_replied', 'desc'),
-                  }"
-                />
-              </div>
-            </div>
-          </th>
-          <th class="px-2 py-4 items-center" colspan="2">
-            <div class="flex items-center">
-              Active
-              <div class="inline-flex flex-col">
-                <icon
-                  name="chevron-up"
-                  @click.native="sort('active', 'asc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('active', 'asc') }"
-                />
-                <icon
-                  name="chevron-down"
-                  @click.native="sort('active', 'desc')"
-                  class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
-                  :class="{ 'text-grey-800': isCurrentSort('active', 'desc') }"
-                />
-              </div>
-            </div>
-          </th>
-        </tr>
-        <tr
-          v-for="alias in queriedAliases"
-          :key="alias.id"
-          class="hover:bg-grey-50 focus-within:bg-grey-50 h-20"
-        >
-          <td class="border-grey-200 border-t">
-            <div class="pl-4 pr-2 py-4 flex items-center">
-              <span
-                class="tooltip outline-none text-sm"
-                :data-tippy-content="alias.created_at | formatDate"
-                >{{ alias.created_at | timeAgo }}</span
-              >
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 flex items-center">
-              <span
-                class="tooltip cursor-pointer outline-none"
-                data-tippy-content="Click to copy"
-                v-clipboard="() => getAliasEmail(alias)"
-                v-clipboard:success="clipboardSuccess"
-                v-clipboard:error="clipboardError"
-              >
-                <span class="font-semibold text-indigo-800">{{
-                  alias.local_part | truncate(36)
-                }}</span>
-                <span class="block text-grey-400 text-sm">{{
-                  getAliasEmail(alias) | truncate(40)
-                }}</span>
-              </span>
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 flex items-center">
-              <span
-                v-if="alias.recipients.length && alias.id !== recipientsAliasToEdit.id"
-                class="tooltip outline-none"
-                :data-tippy-content="recipientsTooltip(alias.recipients)"
-                >{{ alias.recipients[0].email | truncate(25) }}
-                <span
-                  v-if="alias.recipients.length > 1"
-                  class="block text-center text-grey-500 text-sm"
-                >
-                  + {{ alias.recipients.length - 1 }}</span
-                >
-              </span>
-              <span v-else-if="alias.id === recipientsAliasToEdit.id">{{
-                aliasRecipientsToEdit.length ? aliasRecipientsToEdit.length : '1'
-              }}</span>
-              <span
-                v-else
-                class="py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
-                :data-tippy-content="defaultRecipient.email"
-                >default</span
-              >
 
-              <icon
-                name="edit"
-                class="ml-2 block w-6 h-6 text-grey-200 fill-current cursor-pointer"
-                @click.native="openAliasRecipientsModal(alias)"
-              />
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 text-sm">
-              <div
-                v-if="aliasIdToEdit === alias.id"
-                class="w-full flex items-center justify-between"
-              >
-                <input
-                  @keyup.enter="editAlias(alias)"
-                  @keyup.esc="aliasIdToEdit = aliasDescriptionToEdit = ''"
-                  v-model="aliasDescriptionToEdit"
-                  type="text"
-                  class="appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
-                  :class="
-                    aliasDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'
-                  "
-                  placeholder="Add description"
-                  tabindex="0"
-                  autofocus
-                />
-                <icon
-                  name="close"
-                  class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
-                  @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
-                />
-                <icon
-                  name="save"
-                  class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
-                  @click.native="editAlias(alias)"
-                />
-              </div>
-              <div v-else-if="alias.description" class="flex items-center justify-around">
-                <span
-                  class="tooltip outline-none"
-                  :data-tippy-content="alias.description"
-                  v-clipboard="() => alias.description"
-                  v-clipboard:success="clipboardSuccess"
-                  v-clipboard:error="clipboardError"
-                >
-                  <icon
-                    name="desc"
-                    class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
-                  />
-                </span>
-                <icon
-                  name="edit"
-                  class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
-                  @click.native="
-                    ;(aliasIdToEdit = alias.id), (aliasDescriptionToEdit = alias.description)
-                  "
-                />
-              </div>
-              <div v-else class="w-full flex justify-center">
-                <icon
-                  name="plus"
-                  class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
-                  @click.native="aliasIdToEdit = alias.id"
-                />
-              </div>
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
-              {{ alias.emails_forwarded }}
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
-              {{ alias.emails_blocked }}
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
-              {{ alias.emails_replied }}
-            </div>
-          </td>
-          <td class="border-grey-200 border-t">
-            <div class="px-2 py-4 flex items-center justify-center">
-              <Toggle
-                v-model="alias.active"
-                @on="activateAlias(alias)"
-                @off="deactivateAlias(alias)"
-              />
-            </div>
-          </td>
-          <td class="border-grey-200 border-t w-px">
-            <div class="px-4 flex items-center justify-center outline-none" tabindex="-1">
-              <icon
-                name="trash"
-                class="block w-6 h-6 text-grey-200 fill-current  cursor-pointer"
-                @click.native="openDeleteModal(alias.id)"
-              />
-            </div>
-          </td>
-        </tr>
-        <tr v-if="queriedAliases.length === 0">
-          <td
-            class="border-grey-200 border-t px-6 py-4 text-center h-24 text-lg text-grey-700"
-            colspan="9"
+    <vue-good-table
+      v-if="initialAliases.length"
+      @on-search="debounceToolips"
+      :columns="columns"
+      :rows="rows"
+      :search-options="{
+        enabled: true,
+        skipDiacritics: true,
+        externalQuery: search,
+        placeholder: 'Search aliases',
+      }"
+      :sort-options="{
+        enabled: true,
+      }"
+      styleClass="vgt-table"
+    >
+      <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
+        No aliases found for that search!
+      </div>
+      <template slot="table-row" slot-scope="props">
+        <span
+          v-if="props.column.field == 'created_at'"
+          class="tooltip outline-none text-sm"
+          :data-tippy-content="props.row.created_at | formatDate"
+          >{{ props.row.created_at | timeAgo }}
+        </span>
+        <span v-else-if="props.column.field == 'email'" class="block">
+          <span
+            class="text-grey-400 tooltip cursor-pointer outline-none"
+            data-tippy-content="Click to copy"
+            v-clipboard="() => getAliasEmail(props.row)"
+            v-clipboard:success="clipboardSuccess"
+            v-clipboard:error="clipboardError"
+            ><span class="font-semibold text-indigo-800">{{
+              getAliasLocalPart(props.row) | truncate(60)
+            }}</span
+            ><span v-if="getAliasLocalPart(props.row).length <= 60">{{
+              ('@' + props.row.domain) | truncate(60 - getAliasLocalPart(props.row).length)
+            }}</span>
+          </span>
+          <div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
+            <input
+              @keyup.enter="editAlias(rows[props.row.originalIndex])"
+              @keyup.esc="aliasIdToEdit = aliasDescriptionToEdit = ''"
+              v-model="aliasDescriptionToEdit"
+              type="text"
+              class="flex-grow text-sm appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
+              :class="aliasDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'"
+              placeholder="Add description"
+              tabindex="0"
+              autofocus
+            />
+            <icon
+              name="close"
+              class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
+              @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
+            />
+            <icon
+              name="save"
+              class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
+              @click.native="editAlias(rows[props.row.originalIndex])"
+            />
+          </div>
+          <div v-else-if="props.row.description" class="flex items-center">
+            <span class="inline-block text-grey-400 text-sm py-1 border border-transparent">
+              {{ props.row.description | truncate(60) }}
+            </span>
+            <icon
+              name="edit"
+              class="inline-block w-6 h-6 ml-2 text-grey-200 fill-current cursor-pointer"
+              @click.native="
+                ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
+              "
+            />
+          </div>
+          <div v-else>
+            <span
+              class="inline-block text-grey-200 text-sm cursor-pointer py-1 border border-transparent"
+              @click="aliasIdToEdit = props.row.id"
+              >Add description</span
+            >
+          </div>
+        </span>
+        <span
+          v-else-if="props.column.field == 'recipients'"
+          class="flex items-center justify-center"
+        >
+          <span
+            v-if="props.row.recipients.length && props.row.id !== recipientsAliasToEdit.id"
+            class="inline-block tooltip outline-none font-semibold text-indigo-800"
+            :data-tippy-content="recipientsTooltip(props.row.recipients)"
+          >
+            {{ props.row.recipients.length }}
+          </span>
+          <span v-else-if="props.row.id === recipientsAliasToEdit.id">{{
+            aliasRecipientsToEdit.length ? aliasRecipientsToEdit.length : '1'
+          }}</span>
+          <span
+            v-else
+            class="py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
+            :data-tippy-content="defaultRecipient.email"
+            >default</span
           >
-            No aliases found for that search!
-          </td>
-        </tr>
-      </table>
 
-      <div v-else class="p-8 text-center text-lg text-grey-700">
+          <icon
+            name="edit"
+            class="ml-2 inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
+            @click.native="openAliasRecipientsModal(props.row)"
+          />
+        </span>
+        <span
+          v-else-if="props.column.field == 'emails_forwarded'"
+          class="font-semibold text-indigo-800"
+        >
+          {{ props.row.emails_forwarded }}
+        </span>
+        <span
+          v-else-if="props.column.field == 'emails_blocked'"
+          class="font-semibold text-indigo-800"
+        >
+          {{ props.row.emails_blocked }}
+        </span>
+        <span
+          v-else-if="props.column.field == 'emails_replied'"
+          class="font-semibold text-indigo-800"
+        >
+          {{ props.row.emails_replied }}
+        </span>
+        <span v-else-if="props.column.field === 'active'" class="flex items-center justify-center">
+          <Toggle
+            v-model="rows[props.row.originalIndex].active"
+            @on="activateAlias(props.row.id)"
+            @off="deactivateAlias(props.row.id)"
+          />
+        </span>
+        <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
+          <icon
+            name="trash"
+            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
+            @click.native="openDeleteModal(props.row.id)"
+          />
+        </span>
+      </template>
+    </vue-good-table>
+    <div v-else class="bg-white rounded shadow overflow-x-auto">
+      <div class="p-8 text-center text-lg text-grey-700">
         <h1 class="mb-6 text-2xl text-indigo-800 font-semibold">
           It doesn't look like you have any aliases yet!
         </h1>
@@ -710,7 +533,6 @@ export default {
   },
   data() {
     return {
-      aliases: this.initialAliases,
       search: '',
       aliasIdToEdit: '',
       aliasDescriptionToEdit: '',
@@ -726,12 +548,67 @@ export default {
       generateAliasDomain: this.domain,
       recipientsAliasToEdit: {},
       aliasRecipientsToEdit: [],
+      columns: [
+        {
+          label: 'Created',
+          field: 'created_at',
+          globalSearchDisabled: true,
+        },
+        {
+          label: 'Alias',
+          field: 'email',
+        },
+        {
+          label: 'Recipients',
+          field: 'recipients',
+          tdClass: 'text-center',
+          sortable: true,
+          sortFn: this.sortRecipients,
+        },
+        {
+          label: 'Description',
+          field: 'description',
+          sortable: false,
+          hidden: true,
+        },
+        {
+          label: 'Forwarded',
+          field: 'emails_forwarded',
+          type: 'number',
+          tdClass: 'text-center',
+          globalSearchDisabled: true,
+        },
+        {
+          label: 'Blocked',
+          field: 'emails_blocked',
+          type: 'number',
+          tdClass: 'text-center',
+          globalSearchDisabled: true,
+        },
+        {
+          label: 'Replies',
+          field: 'emails_replied',
+          type: 'number',
+          tdClass: 'text-center',
+          globalSearchDisabled: true,
+        },
+        {
+          label: 'Active',
+          field: 'active',
+          type: 'boolean',
+          globalSearchDisabled: true,
+        },
+        {
+          label: '',
+          field: 'actions',
+          sortable: false,
+          globalSearchDisabled: true,
+        },
+      ],
+      rows: this.initialAliases,
     }
   },
   watch: {
-    queriedAliases: _.debounce(function() {
-      this.addTooltips()
-    }, 50),
     aliasIdToEdit: _.debounce(function() {
       this.addTooltips()
     }, 50),
@@ -740,14 +617,14 @@ export default {
     }, 50),
   },
   computed: {
-    queriedAliases() {
-      return _.filter(this.aliases, alias => alias.email.includes(this.search))
+    activeUuidAliases() {
+      return _.filter(this.rows, alias => alias.id === alias.local_part && alias.active)
     },
     totalActive() {
-      return _.filter(this.aliases, 'active').length
+      return _.filter(this.rows, 'active').length
     },
     totalInactive() {
-      return _.reject(this.aliases, 'active').length
+      return _.reject(this.rows, 'active').length
     },
   },
   methods: {
@@ -757,12 +634,12 @@ export default {
         arrowType: 'round',
       })
     },
+    debounceToolips: _.debounce(function() {
+      this.addTooltips()
+    }, 50),
     recipientsTooltip(recipients) {
       return _.reduce(recipients, (list, recipient) => list + `${recipient.email}<br>`, '')
     },
-    isCurrentSort(col, dir) {
-      return this.currentSort === col && this.currentSortDir === dir
-    },
     openDeleteModal(id) {
       this.deleteAliasModalOpen = true
       this.aliasIdToDelete = id
@@ -777,7 +654,7 @@ export default {
       axios
         .delete(`/aliases/${id}`)
         .then(response => {
-          this.aliases = _.filter(this.aliases, aliases => aliases.id !== id)
+          this.rows = _.reject(this.rows, alias => alias.id === id)
           this.deleteAliasModalOpen = false
           this.deleteAliasLoading = false
         })
@@ -787,20 +664,6 @@ export default {
           this.deleteAliasLoading = false
         })
     },
-    sort(col, dir) {
-      if (this.currentSort === col && this.currentSortDir === dir) {
-        this.currentSort = 'created_at'
-        this.currentSortDir = 'desc'
-      } else {
-        this.currentSort = col
-        this.currentSortDir = dir
-      }
-
-      this.aliases = _.orderBy(this.aliases, [this.currentSort], [this.currentSortDir])
-    },
-    reSort() {
-      this.aliases = _.orderBy(this.aliases, [this.currentSort], [this.currentSortDir])
-    },
     openAliasRecipientsModal(alias) {
       this.editAliasRecipientsModalOpen = true
       this.recipientsAliasToEdit = alias
@@ -826,7 +689,7 @@ export default {
           }
         )
         .then(response => {
-          let alias = _.find(this.aliases, ['id', this.recipientsAliasToEdit.id])
+          let alias = _.find(this.rows, ['id', this.recipientsAliasToEdit.id])
           alias.recipients = this.aliasRecipientsToEdit
 
           this.editAliasRecipientsModalOpen = false
@@ -858,8 +721,7 @@ export default {
         )
         .then(({ data }) => {
           this.generateAliasLoading = false
-          this.aliases.push(data.data)
-          this.reSort()
+          this.rows.push(data.data)
           this.generateAliasModalOpen = false
           this.success('New alias generated successfully')
         })
@@ -899,12 +761,12 @@ export default {
           this.error()
         })
     },
-    activateAlias(alias) {
+    activateAlias(id) {
       axios
         .post(
           `/active-aliases`,
           JSON.stringify({
-            id: alias.id,
+            id: id,
           }),
           {
             headers: { 'Content-Type': 'application/json' },
@@ -917,9 +779,9 @@ export default {
           this.error()
         })
     },
-    deactivateAlias(alias) {
+    deactivateAlias(id) {
       axios
-        .delete(`/active-aliases/${alias.id}`)
+        .delete(`/active-aliases/${id}`)
         .then(response => {
           //
         })
@@ -932,6 +794,12 @@ export default {
         ? `${alias.local_part}+${alias.extension}@${alias.domain}`
         : alias.email
     },
+    getAliasLocalPart(alias) {
+      return alias.extension ? `${alias.local_part}+${alias.extension}` : alias.local_part
+    },
+    sortRecipients(x, y) {
+      return x.length < y.length ? -1 : x.length > y.length ? 1 : 0
+    },
     clipboardSuccess() {
       this.success('Copied to clipboard')
     },