Преглед изворни кода

Added ability to view and restore deleted aliases

Will пре 5 година
родитељ
комит
2359d42077

+ 2 - 2
app/Http/Controllers/Api/ActiveAliasController.php

@@ -10,7 +10,7 @@ class ActiveAliasController extends Controller
 {
     public function store(Request $request)
     {
-        $alias = user()->aliases()->findOrFail($request->id);
+        $alias = user()->aliases()->withTrashed()->findOrFail($request->id);
 
         $alias->activate();
 
@@ -19,7 +19,7 @@ class ActiveAliasController extends Controller
 
     public function destroy($id)
     {
-        $alias = user()->aliases()->findOrFail($id);
+        $alias = user()->aliases()->withTrashed()->findOrFail($id);
 
         $alias->deactivate();
 

+ 24 - 4
app/Http/Controllers/Api/AliasController.php

@@ -8,19 +8,30 @@ use App\Http\Controllers\Controller;
 use App\Http\Requests\StoreAliasRequest;
 use App\Http\Requests\UpdateAliasRequest;
 use App\Http\Resources\AliasResource;
+use Illuminate\Http\Request;
 use Illuminate\Support\Str;
 use Ramsey\Uuid\Uuid;
 
 class AliasController extends Controller
 {
-    public function index()
+    public function index(Request $request)
     {
-        return AliasResource::collection(user()->aliases()->with('recipients')->latest()->get());
+        $aliases = user()->aliases()->with('recipients')->latest();
+
+        if ($request->deleted === 'with') {
+            $aliases->withTrashed();
+        }
+
+        if ($request->deleted === 'only') {
+            $aliases->onlyTrashed();
+        }
+
+        return AliasResource::collection($aliases->get());
     }
 
     public function show($id)
     {
-        $alias = user()->aliases()->findOrFail($id);
+        $alias = user()->aliases()->withTrashed()->findOrFail($id);
 
         return new AliasResource($alias->load('recipients'));
     }
@@ -83,13 +94,22 @@ class AliasController extends Controller
 
     public function update(UpdateAliasRequest $request, $id)
     {
-        $alias = user()->aliases()->findOrFail($id);
+        $alias = user()->aliases()->withTrashed()->findOrFail($id);
 
         $alias->update(['description' => $request->description]);
 
         return new AliasResource($alias->refresh()->load('recipients'));
     }
 
+    public function restore($id)
+    {
+        $alias = user()->aliases()->withTrashed()->findOrFail($id);
+
+        $alias->restore();
+
+        return new AliasResource($alias->refresh()->load('recipients'));
+    }
+
     public function destroy($id)
     {
         $alias = user()->aliases()->findOrFail($id);

+ 1 - 1
app/Http/Controllers/Api/AliasRecipientController.php

@@ -10,7 +10,7 @@ class AliasRecipientController extends Controller
 {
     public function store(StoreAliasRecipientRequest $request)
     {
-        $alias = user()->aliases()->findOrFail($request->alias_id);
+        $alias = user()->aliases()->withTrashed()->findOrFail($request->alias_id);
 
         $alias->recipients()->sync($request->recipient_ids);
 

+ 2 - 0
app/Http/Resources/AliasResource.php

@@ -22,9 +22,11 @@ class AliasResource extends JsonResource
             'emails_forwarded' => $this->emails_forwarded,
             'emails_blocked' => $this->emails_blocked,
             'emails_replied' => $this->emails_replied,
+            'emails_sent' => $this->emails_sent,
             'recipients' => RecipientResource::collection($this->whenLoaded('recipients')),
             'created_at' => $this->created_at->toDateTimeString(),
             'updated_at' => $this->updated_at->toDateTimeString(),
+            'deleted_at' => $this->deleted_at ? $this->deleted_at->toDateTimeString() : null,
         ];
     }
 }

+ 17 - 17
composer.lock

@@ -800,16 +800,16 @@
         },
         {
             "name": "egulias/email-validator",
-            "version": "2.1.17",
+            "version": "2.1.18",
             "source": {
                 "type": "git",
                 "url": "https://github.com/egulias/EmailValidator.git",
-                "reference": "ade6887fd9bd74177769645ab5c474824f8a418a"
+                "reference": "cfa3d44471c7f5bfb684ac2b0da7114283d78441"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a",
-                "reference": "ade6887fd9bd74177769645ab5c474824f8a418a",
+                "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/cfa3d44471c7f5bfb684ac2b0da7114283d78441",
+                "reference": "cfa3d44471c7f5bfb684ac2b0da7114283d78441",
                 "shasum": ""
             },
             "require": {
@@ -833,7 +833,7 @@
             },
             "autoload": {
                 "psr-4": {
-                    "Egulias\\EmailValidator\\": "EmailValidator"
+                    "Egulias\\EmailValidator\\": "src"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -854,7 +854,7 @@
                 "validation",
                 "validator"
             ],
-            "time": "2020-02-13T22:36:52+00:00"
+            "time": "2020-06-16T20:11:17+00:00"
         },
         {
             "name": "fideloper/proxy",
@@ -1038,16 +1038,16 @@
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "6.5.4",
+            "version": "6.5.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "a4a1b6930528a8f7ee03518e6442ec7a44155d9d"
+                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a4a1b6930528a8f7ee03518e6442ec7a44155d9d",
-                "reference": "a4a1b6930528a8f7ee03518e6442ec7a44155d9d",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+                "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
                 "shasum": ""
             },
             "require": {
@@ -1055,7 +1055,7 @@
                 "guzzlehttp/promises": "^1.0",
                 "guzzlehttp/psr7": "^1.6.1",
                 "php": ">=5.5",
-                "symfony/polyfill-intl-idn": "1.17.0"
+                "symfony/polyfill-intl-idn": "^1.17.0"
             },
             "require-dev": {
                 "ext-curl": "*",
@@ -1101,7 +1101,7 @@
                 "rest",
                 "web service"
             ],
-            "time": "2020-05-25T19:35:05+00:00"
+            "time": "2020-06-16T21:01:06+00:00"
         },
         {
             "name": "guzzlehttp/promises",
@@ -2553,16 +2553,16 @@
         },
         {
             "name": "opis/closure",
-            "version": "3.5.4",
+            "version": "3.5.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/opis/closure.git",
-                "reference": "1d0deef692f66dae5d70663caee2867d0971306b"
+                "reference": "dec9fc5ecfca93f45cd6121f8e6f14457dff372c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/opis/closure/zipball/1d0deef692f66dae5d70663caee2867d0971306b",
-                "reference": "1d0deef692f66dae5d70663caee2867d0971306b",
+                "url": "https://api.github.com/repos/opis/closure/zipball/dec9fc5ecfca93f45cd6121f8e6f14457dff372c",
+                "reference": "dec9fc5ecfca93f45cd6121f8e6f14457dff372c",
                 "shasum": ""
             },
             "require": {
@@ -2610,7 +2610,7 @@
                 "serialization",
                 "serialize"
             ],
-            "time": "2020-06-07T11:41:29+00:00"
+            "time": "2020-06-17T14:59:55+00:00"
         },
         {
             "name": "paragonie/constant_time_encoding",

+ 6 - 0
resources/js/components/Icon.vue

@@ -116,6 +116,12 @@
     ></path>
   </svg>
 
+  <svg v-else-if="name === 'undo'" xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -2 24 24">
+    <path
+      d="M5.308 7.612l1.352-.923a.981.981 0 0 1 1.372.27 1.008 1.008 0 0 1-.266 1.388l-3.277 2.237a.981.981 0 0 1-1.372-.27L.907 6.998a1.007 1.007 0 0 1 .266-1.389.981.981 0 0 1 1.372.27l.839 1.259C4.6 3.01 8.38 0 12.855 0c5.458 0 9.882 4.477 9.882 10s-4.424 10-9.882 10a.994.994 0 0 1-.988-1c0-.552.443-1 .988-1 4.366 0 7.906-3.582 7.906-8s-3.54-8-7.906-8C9.311 2 6.312 4.36 5.308 7.612z"
+    ></path>
+  </svg>
+
   <svg
     v-else-if="name === 'blocked'"
     xmlns="http://www.w3.org/2000/svg"

+ 121 - 10
resources/js/pages/Aliases.vue

@@ -118,13 +118,39 @@
           class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
         />
       </div>
-      <div class="mt-4 md:mt-0">
-        <button
-          @click="generateAliasModalOpen = true"
-          class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
-        >
-          Generate New Alias
-        </button>
+      <div class="flex flex-wrap mt-4 md:mt-0">
+        <div class="block relative mr-4">
+          <select
+            v-model="showAliases"
+            class="block appearance-none w-full text-grey-700 bg-white p-3 pr-8 rounded shadow focus:shadow-outline"
+            required
+          >
+            <option value="without">Hide Deleted</option>
+            <option value="with">Show Deleted</option>
+            <option value="only">Deleted Only</option>
+          </select>
+          <div
+            class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
+          >
+            <svg
+              class="fill-current h-4 w-4"
+              xmlns="http://www.w3.org/2000/svg"
+              viewBox="0 0 20 20"
+            >
+              <path
+                d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
+              />
+            </svg>
+          </div>
+        </div>
+        <div>
+          <button
+            @click="generateAliasModalOpen = true"
+            class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
+          >
+            Generate New Alias
+          </button>
+        </div>
       </div>
     </div>
 
@@ -279,8 +305,15 @@
         </span>
         <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
           <icon
+            v-if="props.row.deleted_at"
+            name="undo"
+            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
+            @click.native="openRestoreModal(props.row.id)"
+          />
+          <icon
+            v-else
             name="trash"
-            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
+            class="block w-6 h-6 text-grey-200 fill-current cursor-pointer outline-none"
             @click.native="openDeleteModal(props.row.id)"
           />
         </span>
@@ -493,6 +526,38 @@
       </div>
     </Modal>
 
+    <Modal :open="restoreAliasModalOpen" @close="closeRestoreModal">
+      <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
+        <h2
+          class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
+        >
+          Restore alias
+        </h2>
+        <p class="mt-4 text-grey-700">
+          Are you sure you want to restore this alias? Once restored, this alias will
+          <b>be able to receive emails again</b>.
+        </p>
+        <div class="mt-6">
+          <button
+            type="button"
+            @click="restoreAlias(aliasIdToRestore)"
+            class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
+            :class="restoreAliasLoading ? 'cursor-not-allowed' : ''"
+            :disabled="restoreAliasLoading"
+          >
+            Restore alias
+            <loader v-if="restoreAliasLoading" />
+          </button>
+          <button
+            @click="closeRestoreModal"
+            class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
+          >
+            Cancel
+          </button>
+        </div>
+      </div>
+    </Modal>
+
     <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
       <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
         <h2
@@ -501,8 +566,8 @@
           Delete alias
         </h2>
         <p class="mt-4 text-grey-700">
-          Are you sure you want to delete this alias? This action cannot be undone. Once deleted,
-          this alias will <b>not be able to be used again</b> and will reject any emails sent to it.
+          Are you sure you want to delete this alias? <b>You can restore this alias</b> if you later
+          change your mind. Once deleted, this alias will <b>reject any emails sent to it</b>.
         </p>
         <div class="mt-6">
           <button
@@ -595,11 +660,15 @@ export default {
   data() {
     return {
       search: '',
+      showAliases: 'without',
       aliasIdToEdit: '',
       aliasDescriptionToEdit: '',
       aliasIdToDelete: '',
+      aliasIdToRestore: '',
       deleteAliasLoading: false,
       deleteAliasModalOpen: false,
+      restoreAliasLoading: false,
+      restoreAliasModalOpen: false,
       editAliasRecipientsLoading: false,
       editAliasRecipientsModalOpen: false,
       generateAliasModalOpen: false,
@@ -688,6 +757,9 @@ export default {
     editAliasRecipientsModalOpen: _.debounce(function() {
       this.addTooltips()
     }, 50),
+    showAliases(value) {
+      this.updateAliases()
+    },
   },
   computed: {
     activeUuidAliases() {
@@ -721,6 +793,26 @@ export default {
       this.deleteAliasModalOpen = false
       this.aliasIdToDelete = ''
     },
+    openRestoreModal(id) {
+      this.restoreAliasModalOpen = true
+      this.aliasIdToRestore = id
+    },
+    closeRestoreModal() {
+      this.restoreAliasModalOpen = false
+      this.aliasIdToRestore = ''
+    },
+    updateAliases() {
+      axios
+        .get(`/api/v1/aliases?deleted=${this.showAliases}`, {
+          headers: { 'Content-Type': 'application/json' },
+        })
+        .then(response => {
+          this.rows = response.data.data
+        })
+        .catch(error => {
+          this.error()
+        })
+    },
     deleteAlias(id) {
       this.deleteAliasLoading = true
 
@@ -737,6 +829,25 @@ export default {
           this.deleteAliasLoading = false
         })
     },
+    restoreAlias(id) {
+      this.restoreAliasLoading = true
+
+      axios
+        .patch(`/api/v1/aliases/${id}/restore`, {
+          headers: { 'Content-Type': 'application/json' },
+        })
+        .then(response => {
+          this.updateAliases()
+          this.restoreAliasModalOpen = false
+          this.restoreAliasLoading = false
+          this.success('Alias restored successfully')
+        })
+        .catch(error => {
+          this.error()
+          this.restoreAliasModalOpen = false
+          this.restoreAliasLoading = false
+        })
+    },
     openAliasRecipientsModal(alias) {
       this.editAliasRecipientsModalOpen = true
       this.recipientsAliasToEdit = alias

+ 1 - 0
routes/api.php

@@ -19,6 +19,7 @@ Route::group([
     Route::get('/aliases/{id}', 'Api\AliasController@show');
     Route::post('/aliases', 'Api\AliasController@store');
     Route::patch('/aliases/{id}', 'Api\AliasController@update');
+    Route::patch('/aliases/{id}/restore', 'Api\AliasController@restore');
     Route::delete('/aliases/{id}', 'Api\AliasController@destroy');
 
     Route::post('/active-aliases', 'Api\ActiveAliasController@store');

+ 56 - 0
tests/Feature/Api/AliasesTest.php

@@ -34,6 +34,48 @@ class AliasesTest extends TestCase
         $this->assertCount(3, $response->json()['data']);
     }
 
+    /** @test */
+    public function user_can_get_all_aliases_including_deleted()
+    {
+        // Arrange
+        factory(Alias::class, 2)->create([
+            'user_id' => $this->user->id
+        ]);
+
+        factory(Alias::class)->create([
+            'user_id' => $this->user->id,
+            'deleted_at' => now()
+        ]);
+
+        // Act
+        $response = $this->get('/api/v1/aliases?deleted=with');
+
+        // Assert
+        $response->assertSuccessful();
+        $this->assertCount(3, $response->json()['data']);
+    }
+
+    /** @test */
+    public function user_can_get_only_deleted_aliases()
+    {
+        // Arrange
+        factory(Alias::class, 2)->create([
+            'user_id' => $this->user->id,
+            'deleted_at' => now()
+        ]);
+
+        factory(Alias::class)->create([
+            'user_id' => $this->user->id
+        ]);
+
+        // Act
+        $response = $this->get('/api/v1/aliases?deleted=only');
+
+        // Assert
+        $response->assertSuccessful();
+        $this->assertCount(2, $response->json()['data']);
+    }
+
     /** @test */
     public function user_can_get_individual_alias()
     {
@@ -133,6 +175,20 @@ class AliasesTest extends TestCase
         $this->assertEmpty($this->user->aliases);
     }
 
+    /** @test */
+    public function user_can_restore_deleted_alias()
+    {
+        $alias = factory(Alias::class)->create([
+            'user_id' => $this->user->id,
+            'deleted_at' => now()
+        ]);
+
+        $response = $this->json('PATCH', '/api/v1/aliases/'.$alias->id.'/restore');
+
+        $response->assertStatus(200);
+        $this->assertFalse($this->user->aliases[0]->trashed());
+    }
+
     /** @test */
     public function user_can_activate_alias()
     {