Laravel Docker and Breeze Part 2 Tutorial
me@grafxflow

Written by me@grafxflow

06 Jul, 2023

0

885

Laravel, Docker, Breeze and VueJS Advanced CRUD - Part 2

So this is part 2 of the Laravel, Docker and Breeze tutorial - with this we will be doing more advanced things in relation to the Soft Delete for Users.

Here are the steps of the tutorial.

  1. List all the Deleted Users via Soft Delete
  2. Add Recover User and Permanently Delete User Actions

Source code can be found at github link.

Step 1. List all the Deleted Users via Soft Delete

This is more or less a duplication of the Users Index page so lets start by creating the Deleted Users List o lets create the vuejs first 'resources/js/Pages/Users/Trashed.vue'.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';

defineProps({
  users: {
    type: Object,
  },
});

</script>

<template>
  <Head title="Users Trashed" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">Users Trashed</h2>
    </template>

    <div class="py-12">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
          <div class="flex flex-col">
            <div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
              <div class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8">
                <div class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg">

                  <table class="min-w-full divide-y divide-gray-200 table-fixed">
                    <thead class="bg-indigo-500">
                      <tr>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('id')">
                            ID
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('email')">
                            Email
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('name')">
                            Name
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between">
                            Actions
                          </span>
                        </th>
                      </tr>
                    </thead>
                    <tbody class="bg-white divide-y divide-gray-200">
                      <tr v-for="(user, index) in users" :key="user.id">
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.id }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.email }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.name }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          <!-- Start Actions -->
                          <!-- End Actions -->
                        </td>
                      </tr>
                    </tbody>
                  </table>

                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

  </AuthenticatedLayout>
</template>

Then in the web routes update the users with.

// Users CRUD routes
Route::get('/users', [UsersController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
Route::patch('/users/{user}/update', [UsersController::class, 'update'])->name('users.update');
Route::delete('/users/{user}/delete', [UsersController::class, 'delete'])->name('users.delete');
Route::get('/users/create', [UsersController::class, 'create'])->name('users.create');
Route::post('/users/store', [UsersController::class, 'store'])->name('users.store');
Route::get('/users/trashed', [UsersController::class, 'trashed'])->name('users.trashed');

Now in the controller lets give the trashed users table a users object as data - the big difference to the User Index is the use of onlyTrashed which limits to soft deletes of that model.

  /**
   * Display deleted (Soft Delete) from users.
   */
  public function trashed(Request $request): Response
  {
    return Inertia::render('Users/Trashed', [
      'users' => User::onlyTrashed()->get(),
    ]);
  }

And finally add 'Trashed Users' navigation link in 'resources/js/Layouts/AuthenticatedLayout.vue'.

<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
    Dashboard
</NavLink>
<NavLink :href="route('users.index')" :active="route().current('users.index')">
    Active Users
</NavLink>
<NavLink :href="route('users.trashed')" :active="route().current('users.trashed')">
    Trashed Users
</NavLink>

So first Create User via the front end. Then once they appear in the Active Users table simply delete them. Now choose the new 'Trashed User' in the navigation.

Step 2. Add Recover User and Permanently Delete User Actions

Now lets add 2 buttons to the Trashed Users list. They will be 'Recover User' which takes them back to Active Users list and then add 'Permanently Delete' which will ignore the Soft Delete altogether and delete them from the database completely. So lets start by editing 'resources/js/Pages/Users/Trashed.vue' to the add the 2 new buttons and the actions required with the 2 new modals.

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import DangerButton from '@/Components/DangerButton.vue';
import Modal from '@/Components/Modal.vue';
import { Head, useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';

defineProps({
  users: {
    type: Object,
  },
});

const confirmingUserRestore = ref(false);
const confirmingUserPermanentlyDelete = ref(false);
const userId = ref(null);

const form = useForm({});

const confirmUserRestore = (id) => {
  userId.value = id;
  confirmingUserRestore.value = true;
};

const confirmUserPermanentlyDelete = (id) => {
  userId.value = id;
  confirmingUserPermanentlyDelete.value = true;
};

const restoreUser = () => {
  form.patch(route('users.restore', {'user': userId.value}), {
    preserveScroll: true,
    onSuccess: () => closeModal(),
    onFinish: () => form.reset(),
  });
};

const permanentlyDeleteUser = () => {
  form.delete(route('users.destroy', {'user': userId.value}), {
    preserveScroll: true,
    onSuccess: () => closeModal(),
    onFinish: () => form.reset(),
  });
};

const closeModal = () => {
  confirmingUserRestore.value = false;
  confirmingUserPermanentlyDelete.value = false;
  form.reset();
};
</script>

<template>
  <Head title="Users Trashed" />

  <AuthenticatedLayout>
    <template #header>
      <h2 class="font-semibold text-xl text-gray-800 leading-tight">Users Trashed</h2>
    </template>

    <div class="py-12">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
          <div class="flex flex-col">
            <div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
              <div class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8">
                <div class="overflow-hidden border-b border-gray-200 shadow sm:rounded-lg">

                  <table class="min-w-full divide-y divide-gray-200 table-fixed">
                    <thead class="bg-indigo-500">
                      <tr>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('id')">
                            ID
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('email')">
                            Email
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('firstname')">
                            Firstname
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between" @click="sort('lastname')">
                            Lastname
                          </span>
                        </th>
                        <th scope="col" class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase">
                          <span class="inline-flex py-3 px-6 w-full justify-between">
                            Actions
                          </span>
                        </th>
                      </tr>
                    </thead>
                    <tbody class="bg-white divide-y divide-gray-200">
                      <tr v-for="(user, index) in users" :key="user.id">
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.id }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.email }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.firstname }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          {{ user.lastname }}
                        </td>
                        <td class="py-4 px-6 whitespace-nowrap">
                          <!-- Start Actions -->
                          <!-- Start Restore User -->
                          <PrimaryButton @click="confirmUserRestore(user.id)">
                            Restore
                          </PrimaryButton>
                          <!-- End Restore User -->
                          <!-- Start Permanently Delete User -->
                          <DangerButton @click="confirmUserPermanentlyDelete(user.id)">
                            Permanently Delete
                          </DangerButton>
                          <!-- End Permanently Delete User -->
                          <!-- End Actions -->
                        </td>
                      </tr>
                    </tbody>
                  </table>

                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <Modal :show="confirmingUserRestore" @close="closeModal">
      <div class="p-6">
        <h2 class="text-lg font-medium text-gray-900">
          Are you sure you want to restore the user?
        </h2>
        <div class="mt-6 flex justify-end">
          <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
          <PrimaryButton
          class="ml-3"
          :class="{ 'opacity-25': form.processing }"
          :disabled="form.processing"
          @click="restoreUser(id)"
          >
            Restore User
          </PrimaryButton>
        </div>
      </div>
    </Modal>

    <Modal :show="confirmingUserPermanentlyDelete" @close="closeModal">
      <div class="p-6">
        <h2 class="text-lg font-medium text-gray-900">
          Are you sure you want to permanently delete the user?
        </h2>
        <div class="mt-6 flex justify-end">
          <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
          <DangerButton
          class="ml-3"
          :class="{ 'opacity-25': form.processing }"
          :disabled="form.processing"
          @click="permanentlyDeleteUser(id)"
          >
            Permanently Delete
          </DangerButton>
        </div>
      </div>
    </Modal>

  </AuthenticatedLayout>
</template>

Now go back to the UsersController and add the required functions - you will notice the usage of 'forcedelete' which overpowers the soft delete in the User modal and the 'restore' which speaks for itself.

    /**
     * Destroy the user account.
     */
    public function destroy(Request $request): RedirectResponse
    {
        User::withTrashed()->find($request->user)->forcedelete();

        return Redirect::route('users.trashed');
    }

    /**
     * Restore the trashed user.
     */
    public function restore(Request $request): RedirectResponse
    {
      User::withTrashed()->find($request->user)->restore();

      return Redirect::route('users.trashed');
    }

Then back in the web roputes add the new routes for Restire and Restore

// Users CRUD routes
Route::get('/users', [UsersController::class, 'index'])->name('users.index');
Route::get('/users/{user}/edit', [UsersController::class, 'edit'])->name('users.edit');
Route::patch('/users/{user}/update', [UsersController::class, 'update'])->name('users.update');
Route::delete('/users/{user}/delete', [UsersController::class, 'delete'])->name('users.delete');
Route::get('/users/create', [UsersController::class, 'create'])->name('users.create');
Route::post('/users/store', [UsersController::class, 'store'])->name('users.store');
Route::get('/users/trashed', [UsersController::class, 'trashed'])->name('users.trashed');
Route::patch('/users/{user}/restore', [UsersController::class, 'restore'])->name('users.restore');
Route::delete('/users/{user}/destroy', [UsersController::class, 'destroy'])->name('users.destroy');

You should now see the new navigation and when choosing restore see the modal.

And they should appear back on the Active Users List.

And now choose to delete the User again and go back to Trashed Users.

This time choose to Permanently Delete them and confirm it in the modal.

Now when going back to Active Users they won't appear because they have been deleted on the database.

Hope this has been helpful!

Add comment

Smart Search

131 Following
57 Followers

me@grafxflow

Hull, United Kingdom

I am a Full-stack Developer who also started delving into the world of UX/UI Design a few years back. I blog and tweet to hopefully share a little bit of knowledge that can help others around the web. Thanks for stopping by!

Follow