VueJS App Admin Screens

Part 46 of 48 in API Driven Development With Laravel and VueJS
Dan Pastori avatar
Dan Pastori September 11th, 2018
⚡️ Updated content is available We learned a lot since we originally wrote this article. We now have this updated for Laravel 10, Vue 3/NuxtJS 3, and Capacitor 3.

We now have everything in place to add a UI for a user to process actions! We have our permissions set up Laravel Gates and Policies in an API Driven SPA, our routes secured Laravel Admin Routes and Security in a SPA, and now our front end routes secured VueJS Route Permissions, Security and Admin Section. Let’s add a screen to our admin section!

Adding a Screen To Manage Actions

This was the whole goal of the last 3 tutorials, have a way for users, through permissions, to manage data in the app either a direct edit, or through an action. Let’s finally add a UI for this and make our Admin.vue layout a little more user friendly.

Now that we have our Admin.vue layout, I added a few UI specific components.

AdminHeader.vue

I added /resources/assets/js/components/admin/AdminHeader.vue with the following code:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  header{
    background-color: #FFFFFF;
    height: 75px;

    z-index: 9999;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;

    img.logo{
      margin: auto;
      margin-top: 22.5px;
      margin-bottom: 22.5px;
      display: block;
    }

    img.hamburger{
      float: right;
      margin-right: 18px;
      margin-top: 30px;
      cursor: pointer;
    }

    img.avatar{
      float: right;
      margin-right: 20px;
      width: 40px;
      height: 40px;
      border-radius: 20px;
      margin-top: 18px;
    }

    &:after{
      content: "";
      display: table;
      clear: both;
    }
  }

  /* Small only */
  @media screen and (max-width: 39.9375em) {
    nav.top-navigation{
      span.login{
        display: none;
      }

      img.hamburger{
        margin-top: 26px;
      }
    }
  }

  /* Medium only */
  @media screen and (min-width: 40em) and (max-width: 63.9375em) {

  }

  /* Large only */
  @media screen and (min-width: 64em) and (max-width: 74.9375em) {

  }
</style>

<template>
  <header class="admin-header">
    <div class="grid-x">
      <div class="large-4 medium-4 small-4 cell">

      </div>
      <div class="large-4 medium-4 small-4 cell">
        <router-link :to="{ name: 'cafes'}">
          <img src="/img/logo.svg" class="logo"/>
        </router-link>
      </div>
      <div class="large-4 medium-4 small-4 cell">
        <img class="hamburger" src="/img/hamburger.svg" v-on:click="setShowPopOut()"/>
        <img class="avatar" v-if="user != '' && userLoadStatus == 2" :src="user.avatar" v-show="userLoadStatus == 2"/>
        <span class="login" v-if="user == ''" v-on:click="login()">Sign In</span>
      </div>
    </div>
  </header>
</template>

<script>
  export default {
    /*
      Defines the computed properties on the component.
    */
    computed: {
      /*
        Retrieves the User Load Status from Vuex
      */
      userLoadStatus(){
        return this.$store.getters.getUserLoadStatus();
      },

      /*
        Retrieves the User from Vuex
      */
      user(){
        return this.$store.getters.getUser;
      }
    },

    methods: {
      setShowPopOut(){
        this.$store.dispatch( 'toggleShowPopOut', { showPopOut: true } );
      }
    }
  }
</script>

This essentially provides a nice header for our admin section. Notice I have a method to show the popout we use on the front end as well? We will be adding that to the layout. This is good for navigating back and forth between the app and the admin screen.

Navigation.vue

The next component I added was /resources/assets/js/components/admin/Navigation.vue with the following code:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  nav.admin-navigation{
    div.admin-link{
      font-size: 16px;
      font-weight: bold;
      font-family: "Lato", sans-serif;
      text-transform: uppercase;
      padding-top: 15px;
      padding-bottom: 15px;

      a{
        color: black;

        &.router-link-active{
          color: $secondary-color;
        }
      }
    }
  }
</style>

<template>
  <nav class="admin-navigation">
    <div class="admin-link">
      <router-link :to="{ name: 'admin-actions' }">
        Actions
      </router-link>
    </div>
  </nav>
</template>

<script>
  export default {

  }
</script>

These are normal VueJS components like the frontend app. This navigation will be on the left hand side of the admin screen and allow navigation between the sections. I then added both the header and the footer along with a <router-view> to the Admin.vue layout:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div#admin-layout{
    div#page-container{
      margin-top: 75px;
    }
  }
</style>

<template>
  <div id="admin-layout">
    <admin-header></admin-header>

    <success-notification></success-notification>
    <error-notification></error-notification>

    <div class="grid-container" id="page-container">
      <div class="grid-x grid-padding-x">
        <div class="large-3 medium-4 cell">
          <navigation></navigation>
        </div>
        <div class="large-9 medium-8 cell">
          <router-view></router-view>
        </div>
      </div>
    </div>

    <pop-out></pop-out>
  </div>
</template>

<script>
  import AdminHeader from '../components/admin/AdminHeader.vue';
  import Navigation from '../components/admin/Navigation.vue';

  import PopOut from '../components/global/PopOut.vue';
  import SuccessNotification from '../components/global/SuccessNotification.vue';
  import ErrorNotification from '../components/global/ErrorNotification.vue';


  export default {
    components: {
      AdminHeader,
      Navigation,
      PopOut,
      SuccessNotification,
      ErrorNotification
    },

    created(){
      this.$store.dispatch( 'loadBrewMethods' );
    },

    computed: {
      user(){
        return this.$store.getters.getUser;
      }
    }
  }
</script>

Now our admin layout has a popout, a header, some navigation and a few components for notifications. On load, we also load the brew methods to use within the admin section.

You can also see that we have the header above, the navigation on the left, and the router view in the center. The router view in the center is where our children pages will be rendered. This will be slick for managing data. Now it’s time to to add our actions page!

Create our Admin Actions Route

The first thing we will do is add a file for our admin actions screen, so add a file: /resources/assets/js/pages/admin/Actions.vue. This will be the component we will use for the action page. Before we shell this out, let’s add the route.

In your /resources/assets/js/routes.js file add the following route as a child of the /admin route. Remember, we have an Admin.vue layout. Add the following route like this:

... rest of routes
{
  path: '/admin',
  name: 'admin',
  redirect: { name: 'admin-actions' },
  component: Vue.component( 'Admin', require('./layouts/Admin.vue' ) ),
  beforeEnter: requireAuth,
  meta: {
    permission: 'owner'
  },
  children: [
    {
      path: 'actions',
      name: 'admin-actions',
      component: Vue.component( 'AdminActions', require( './pages/admin/Actions.vue' ) ),
      meta: {
        permission: 'owner'
      }
    }
  ]
}

Notice a few things here. First, in our /admin route, we redirected to our new admin-actions page. This is because we have the admin page as a layout. We won’t have any data in the layout, so we go to the top admin-actions page. If anyone accesses /admin we will go right to the actions page. This is super convenient, and the trick that makes a layout work.

The next thing, is we applied a permission of owner to the admin-actions page. If the user is an owner, they will only see pending actions on the companies they own. The admins, and super admins will see all pending actions.

Now back to our Actions.vue page. On this page, we will need all pending actions for the user. This will be loaded from our API routes. Like the front end of the application, you will need an API file, and some state to manage the actions. We will add a sweet trick to the state later on, but for now, we just need to add some state. So to do this, I added the following files to the front end:

/resources/assets/js/api/admin/actions.js

and

/resources/assets/js/modules/admin/actions.js

The first file is the API wrapper for our admin actions API. It should contain the following methods:

/*
  Imports the Roast API URL from the config.
*/
import { ROAST_CONFIG } from '../../config.js';

export default {
  /*
    GET   /api/v1/admin/actions
  */
  getActions: function(){
    return axios.get( ROAST_CONFIG.API_URL + '/admin/actions' );
  },

  /*
    PUT   /admin/v1/admin/actions/{actionID}/approve
  */
  putApproveAction: function( id ){
    return axios.put( ROAST_CONFIG.API_URL + '/admin/actions/'+id+'/approve' );
  },

  /*
    PUT   /admin/v1/admin/actions/{actionID}/deny
  */
  putDenyAction: function( id ){
    return axios.put( ROAST_CONFIG.API_URL + '/admin/actions/'+id+'/deny' );
  }
}

If you need a refresher on JS API routes check out: Build Out API Requests in Javascript – Server Side Up.

The second file is the Vuex module for the admin actions. In that file, add the following code:

/*
|-------------------------------------------------------------------------------
| VUEX modules/admin/actions.js
|-------------------------------------------------------------------------------
| The Vuex data store for the admin actions
*/
import ActionsAPI from '../../api/admin/actions.js';

export const actions = {
  /*
    Defines the state being monitored for the module.
  */
  state: {
    actions: [],
    actionsLoadStatus: 0,

    actionApproveStatus: 0,
    actionDeniedStatus: 0
  },

  actions: {
    loadAdminActions( { commit } ){
      commit( 'setActionsLoadStatus', 1 );

      ActionsAPI.getActions()
        .then( function( response ){
          commit( 'setActions', response.data );
          commit( 'setActionsLoadStatus', 2 );
        })
        .catch( function(){
          commit( 'setActions', [] );
          commit( 'setActionsLoadStatus', 3 );
        });
    },

    approveAction( { commit, state, dispatch }, data ){
      commit( 'setActionApproveStatus', 1 );

      ActionsAPI.putApproveAction( data.id )
        .then( function( response ){
          commit( 'setActionApproveStatus', 2 );
          dispatch( 'loadAdminActions' );
        })
        .catch( function(){
          commit( 'setActionApproveStatus', 3 );
        });

    },

    denyAction( { commit, state, dispatch }, data ){
      commit( 'setActionDeniedStatus', 1 );

      ActionsAPI.putDenyAction( data.id )
        .then( function( response ){
          commit( 'setActionDeniedStatus', 2 );
          dispatch( 'loadAdminActions' );
        })
        .catch( function(){
          commit( 'setActionDeniedStatus', 3 );
        });

    }
  },

  mutations: {
    setActionsLoadStatus( state, status ){
      state.actionsLoadStatus = status;
    },

    setActions( state, actions ){
      state.actions = actions;
    },

    setActionApproveStatus( state, status ){
      state.actionApproveStatus = status;
    },

    setActionDeniedStatus( state, status ){
      state.actionDeniedStatus = status;
    }
  },

  getters: {
    getActions( state ){
      return state.actions;
    },

    getActionsLoadStatus( state ){
      return state.actionsLoadStatus;
    },

    getActionApproveStatus( state ){
      return state.actionApproveStatus;
    },

    getActionDeniedStatus( state ){
      return state.actionDeniedStatus;
    }
  }
}

This contains all of the state for the actions. If you need a refresher on Vuex modules check out: Build a Vuex Module – Server Side Up. The only thing we won’t do is register this in our /resources/assets/js/store.js file like a regular module. That “trick” is coming up next!

Now, let’s head back to our /resources/assets/js/pages/admin/Actions.vue file and shell out the functionality. When this page loads, we will need to load the actions that the user has to process. To do this, we will need to dispatch a Vuex action. In the created() lifecycle hook add the following code:

created(){
  this.$store.dispatch( 'loadAdminActions' );
},

This will call the action on the Vuex Module, which in turn calls the JS API file to load the actions from the server. When they are loaded, they are committed to the state and ready for referencing.

In our computed property add the following code:

computed: {
  actions(){
    return this.$store.getters.getActions;
  }
}

This allows our page access to the actions loaded from the API.

We now have our actions loaded! One more step of displaying them and we have come full circle in our UI! This will make the admin’s job a lot easier!

Adding an Action.vue Component

For each action, we are going to provide a nice UI for the user to approve an action. The first thing we should do for this is add a component: /resources/assets/js/components/admin/actions/Action.vue and import it into our Actions.vue page.

Now, in our Actions.vue page we should have the following code:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div#admin-actions{
    div.actions-header{
      font-family: "Lato", sans-serif;
      border-bottom: 1px solid black;
      font-weight: bold;
      padding-bottom: 10px;
    }

    div.no-actions-available{
      text-align: center;
      font-family: "Lato", sans-serif;
      font-size: 20px;
      padding-top: 20px;
      padding-bottom: 20px;
    }
  }
</style>

<template>
  <div id="admin-actions">
    <div class="grid-container">
      <div class="grid-x">
        <div class="large-12 medium-12 cell">
          <h3 class="page-header">Actions</h3>
        </div>
      </div>
    </div>


    <div class="grid-container">
      <div class="grid-x actions-header">
        <div class="large-3 medium-3 cell">
          Company
        </div>
        <div class="large-3 medium-3 cell">
          Cafe
        </div>
        <div class="large-3 medium-3 cell">
          Type
        </div>
        <div class="large-3 medium-3 cell">
          Actions
        </div>
      </div>
      <action v-for="action in actions"
        :key="action.id"
        :action="action">
      </action>
      <div class="large-12 medium-12 cell no-actions-available" v-show="actions.length == 0">
        All outstanding actions have been processed!
      </div>
    </div>
  </div>
</template>

<script>
  import Action from '../../components/admin/actions/Action.vue';

  export default {
    components: {
      Action
    },

    created(){
      this.$store.dispatch( 'loadAdminActions' );
    },

    computed: {
      actions(){
        return this.$store.getters.getActions;
      }
    }
  }
</script>

You can see we are importing our new Action.vue component and registering it with the page component. We then iterate over the actions loaded and display them in the action component. The Action.vue component displaying the action the user can process accepts a key, required by Vue since it’s a web component and generated by iteration (we bind our action ID to this param), and an action parameter which will be the action we are iterating over.

Now let’s open up: /resources/assets/js/components/admin/actions/Action.vue. This will be where we place our actual UI for each individual action. Our Action.vue component will look like this:

<style lang="scss">
  @import '~@/abstracts/_variables.scss';

  div.action{
    font-family: "Lato", sans-serif;
    border-bottom: 1px solid black;
    padding-bottom: 15px;
    padding-top: 15px;

    span.approve-action{
      font-weight: bold;
      cursor: pointer;
      display: inline-block;
      margin-right: 20px;
    }

    span.deny-action{
      color: $secondary-color;
      font-weight: bold;
      cursor: pointer;
      display: inline-block;
    }

    img.more-info{
      cursor: pointer;
      float: right;
      margin-top: 10px;
      margin-right: 10px;
    }
  }
</style>

<template>
  <div class="action">
    <div class="grid-x">
      <div class="large-3 medium-3 cell">
        {{ action.company != null ? action.company.name : '' }}
      </div>
      <div class="large-3 medium-3 cell">
        {{ action.cafe != null ? action.cafe.location_name : '' }}
      </div>
      <div class="large-3 medium-3 cell">
        {{ type }}
      </div>
      <div class="large-3 medium-3 cell">
        <span class="approve-action" v-on:click="approveAction()">Approve</span>
        <span class="deny-action" v-on:click="denyAction()">Deny</span>
        <span v-on:click="showDetails = !showDetails">
          <img src="/img/more-info-closed.svg" class="more-info" v-show="!showDetails"/>
          <img src="/img/more-info-open.svg" class="more-info" v-show="showDetails"/>
        </span>
      </div>
    </div>
    <div class="grid-x" v-show="showDetails">
      <div class="large-12 medium-12 cell">
        <action-cafe-added v-if="action.type == 'cafe-added'" :action="action"></action-cafe-added>
        <action-cafe-edited v-if="action.type == 'cafe-updated'" :action="action"></action-cafe-edited>
        <action-cafe-deleted v-if="action.type == 'cafe-deleted'" :action="action"></action-cafe-deleted>
      </div>
    </div>
  </div>
</template>

<script>
  import ActionCafeAdded from './ActionCafeAdded.vue';
  import ActionCafeEdited from './ActionCafeEdited.vue';
  import ActionCafeDeleted from './ActionCafeDeleted.vue';

  import { EventBus } from '../../../event-bus.js';

  export default {
    props: ['action'],

    components: {
      ActionCafeAdded,
      ActionCafeEdited,
      ActionCafeDeleted
    },

    data(){
      return {
        showDetails: false
      }
    },

    computed: {
      type(){
        switch( this.action.type ){
          case 'cafe-added':
            return 'New Cafe';
          break;
          case 'cafe-updated':
            return 'Updated Cafe';
          break;
          case 'cafe-deleted':
            return 'Cafe Deleted';
          break;
        }
      },

      actionApproveStatus(){
        return this.$store.getters.getActionApproveStatus;
      },

      actionDeniedStatus(){
        return this.$store.getters.getActionDeniedStatus;
      }
    },

    watch: {
      'actionApprovedStatus': function(){
        if( this.actionApproveStatus == 2 ){
          EventBus.$emit('show-success', {
            notification: 'Action approved successfully!'
          });
        }
      },

      'actionDeniedStatus': function(){
        if( this.actionDeniedStatus == 2 ){
          EventBus.$emit('show-success', {
            notification: 'Action denied successfully!'
          });
        }
      }
    },

    methods: {
      approveAction(){
        this.$store.dispatch( 'approveAction', {
          id: this.action.id
        });
      },

      denyAction(){
        this.$store.dispatch( 'denyAction', {
          id: this.action.id
        });
      }
    }
  }
</script>

The first important thing to note is we watch the actionApproveStatus() and the actionDeniedStatus(). These two statuses will change when successful and us to notify the user. In our watch section, we watch both of these. When they change to 2 which means the action was either approved or denied successfully, we will notify the user.

Next, we have two methods, approveAction() and denyAction(). All of these tutorials essentially lead to these methods! Each of these methods sends a request to the API from the user to approve or deny a specific action. When the button is clicked on the action screen, we dispatch an event to the Vuex module for the actions that calls our API. The data we pass along is the id of the action to approve or deny.

Now in this component, there are 3 sub components and a piece of data that shows details or not. I won’t dive into them since they are specific to the functionality of roast and out of scope of permissions. What they do is show the cafe data that’s been added, the differences between the cafe and what was updated, and the cafe data regarding the deleted cafe. If you have any questions about these, leave a comment. You can also see them on GitHub here: GitHub – serversideup/roastandbrew: Roast helps coffee enthusiasts find their next cup of coffee. It’s also open source to help aspiring developers build a single page app on web and mobile. All tutorials can be found at https://srvrsi.de/roast.

To finish off, we have our Vuex Modules trick!

Dynamic Vuex Modules

Remember the sweet state trick I mentioned? Here it is. I mentioned earlier on that an admin side of the app is essentially a whole new app (or at least a huge part of the existing app). Now storing all of the data for the app and the admin section could get unruly. As the app grows you will have more and more data to store. AND for unauthenticated or unauthorized users, they won’t even need the admin side data store. This is where registering and unregistering Vuex modules based on the location of the user within the app comes into play. With our store, we can register certain modules before they are needed and unregister modules when they are no longer needed!

This way, we don’t even have the admin modules accessible if they aren’t needed. The docs for this are here: Vuex | Modules I’ll show you how I implemented them.

Back to our layouts, Layout.vue and Admin.vue, these will be perfect places to do the registering and the unregistering. They essentially configure the sections before the user sees the screen they want.

First, open up /resources/assets/js/layouts/Admin.vue. We will need to import the module for actions (remember we didn’t register it in the global store? this is why).

/*
  Import admin Vuex modules
*/
import { actions } from '../modules/admin/actions.js'

As we grow our admin side, we will import more modules. We will also check for proper security because not all users have permissions to manage users. This will be coming soon.

Next, In the created() lifecycle hook add the following code:

created(){  
  if( !this.$store._modules.get(['admin'] ) ){
    this.$store.registerModule( 'admin', {} );
  }

  if( !this.$store._modules.get(['admin', 'actions']) ){
    this.$store.registerModule( ['admin', 'actions'], actions );
  }
},

What this does is check to see if an admin module is registered. If not, we register an admin module first which is the parent for the rest of the modules.

Next, we check to see if there’s an actions module on the admin module. If there isn’t, we register it on the admin modules.

The registerModule() method accepts 2 parameters. First can be a string or an array of where to register the module. A string will register it on that string in the store. The array will go in order and piece them together. In this case, we are registering our imported actions module at admin.actions in the store. The second parameter is the action actions Vuex store that we imported.

Now open the /resources/assets/js/layouts/Layout.vue file and add the following code to it’s created method:

if( this.$store._modules.get(['admin'] ) ){
  this.$store.unregisterModule( 'admin', {} );
}

Since admin is the parent of the admin modules’ scope, we remove just that and all other modules will be removed as well. If you check your Vuex state in your Vue Dev Tools plugin, you will see when you go back to the app, the admin modules are removed, where when you go to the admin side of the app, they are added.

I like this because vue will not have any excess data lying around if it doesn’t need to and it scopes everything really nicely!

Conclusion

So we covered the entire process of securing the VueJS routes using the permissions in our system in the last tutorial Laravel Admin Routes and Security in a SPA. In this tutorial, we actually added an admin page. The only other changes I made were a few tweaks such as adding a link to the admin section on the side popout:

<div class="side-bar-link" v-if="user != '' && userLoadStatus == 2 && user.permission >= 1">
    <router-link :to="{ name: 'admin'}" v-on:click.native="hideNav()">Admin</router-link>
</div>

And adding a small SASS file for the header on the admin section at /resources/assets/sass/components/admin/_page-header.scss

The last few tutorials may have been a longer tutorial but it sets us up for many new features both on the administration side and on the app side. The next tutorial will be adding some admin level features such as user management and a companies screen. If there are any questions about the admin side so far, or any questions in general, definitely leave a comment below! If you want to learn more about VueJS Route Permissions and Security and want to see more examples of an administration section, sign up to be notified when we launch our book! We will be going through a lot more complex examples: https://serversideup.us2.list-manage.com/subscribe?u=a27137fc57d223f1cc7b986db&id=1276f15943!

Keep Reading
View the course View the Course API Driven Development With Laravel and VueJS
Up Next → Vue Router Permission Recipes and Laravel Policies Examples

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.