Lesson 19

[Sprint Three] View Feedback

Another custom component and modifying state

PRO

Lesson Outline

Feat: View feedback that has been submitted anonymously

Now we can move on to our final user story for this sprint - we need a way for the admin to actually view the feedback that has been submitted.

User story: As a massage therapist, I want the ability to view feedback that has been submitted anonymously, so that I can use that information to improve my services


Project management

Remember to move the card for this task to the Test column, and create a new task branch to work on.


Again, this isn't a feature that we have already accounted for in our initial wireframe so we will need to decide where in the admin section the massage therapist will be able to retrieve this feedback.

At the moment in the admin interface we have a list of clients and then the ability to click a client to view them. We also have two buttons at the top, one for triggering the logout and one for triggering the add new client page:

Client Dashboard

We don't want to store feedback inside of a particular client because it is anonymous, so we are going to need a way to navigate to the view feedback page from the main dashboard that we are looking at above.

Some approaches we could consider are:

  1. Adding a side menu
  2. Adding tabs
  3. Adding an additional button

I won't delve too much into how to decide what approach might be best for navigation, as we cover that quite extensively in Navigation Concepts in the UI/UX module.

Viewing the feedback is a relatively minor feature, and I don't really want to go to the extreme of drastically changing the navigation architecture to facilitate it. What I am going to do is add a FAB button in the bottom-right corner of the main admin dashboard that will launch the view feedback page.

This is somewhat limiting. If we were to add even more pages in the future it might become necessary to re-architect into a tab-based approach or something like that, but for now this will work fine and at least right now I'm not anticipating needing to add more sections of the application.

Let's start by adding an E2E test for this user story. Although we already have a feedback.cy.ts file, the test we are creating now starts from the /clients page so I am going to utilise that test file instead:

Add the following tests to clients.cy.ts:

  it('can view feedback', () => {
    const feedback = {
      response: '{"someProperty": "someValue"}',
    };

    cy.callFirestore('set', 'feedback/abc123', feedback);

    getViewFeedbackButton().click();
    getItemsInFeedbackList().first().click();

    cy.contains('someValue');
  });

  it('can navigate back to the clients page from the feedback page', () => {
    const feedback = {
      response: '{}',
    };

    cy.callFirestore('set', 'feedback/abc123', feedback);

    getViewFeedbackButton().click();
    getItemsInFeedbackList().first().click();

    getViewFeedbackDetailBackButton().click();
    getViewFeedbackBackButton().click();

    getTitle().should('contain.text', 'Clients');
  });

Since we are dealing with a new collection now, we are going to make sure to clean up after ourselves in the beforeEach block:

  beforeEach(() => {
    cy.login();
    cy.visit('/clients');
    cy.callFirestore('delete', 'clients');
    cy.callFirestore('delete', 'feedback');
  });

We will need to add some new utility methods as well:

export const getViewFeedbackButton = () =>
  cy.get('[data-test="view-feedback-button"]');
export const getViewFeedbackBackButton = () =>
  cy.get('[data-test="view-feedback-back-button"]');
export const getViewFeedbackDetailBackButton = () =>
  cy.get('[data-test="view-feedback-detail-back-button"]');
export const getItemsInFeedbackList = () =>
  cy.get('[data-test="feedback-list"] ion-item');

and now let's check that it fails:

Timed out retrying after 4000ms: Expected to find element: [data-test="view-feedback-button"], but never found it.

Great! Now I'm going to jump ahead and implement a few things at once here predicting the future test failures, I am going to:

  • Create the ViewFeedback page that will display a list of all of the available feedback
  • Create the ViewFeedbackDetail page for viewing specific feedback
  • Add the FAB button for launching the ViewFeedback page
ionic g page clients/feature/ClientFeedback --module clients/feature/client-shell/client-shell-routing.module --route-path="feedback"
ionic g page clients/feature/ClientFeedbackDetail --module clients/feature/client-feedback/client-feedback-routing.module --route-path=":id"

The commands above will auto-generate out routes for us, but it is important to keep in mind the order of the routes. This is what our client-shell-routing.module.ts will look like after running the command above:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: () =>
      import('../client-dashboard/client-dashboard.module').then(
        (m) => m.ClientDashboardPageModule
      ),
  },
  {
    path: 'add',
    loadChildren: () =>
      import('../client-add/client-add.module').then(
        (m) => m.ClientAddPageModule
      ),
  },
  {
    path: ':id',
    loadChildren: () =>
      import('../client-detail/client-detail.module').then(
        (m) => m.ClientDetailPageModule
      ),
  },
  {
    path: 'feedback',
    loadChildren: () => import('../client-feedback/client-feedback.module').then( m => m.ClientFeedbackPageModule)
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ClientShellRoutingModule {}

This is a problem because feedback comes after our :id route. This means that when we go to a URL with feedback it is just going to treat that like an id and activate the detail page. We need to make sure to move our feedback route above the :id route:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    loadChildren: () =>
      import('../client-dashboard/client-dashboard.module').then(
        (m) => m.ClientDashboardPageModule
      ),
  },
  {
    path: 'add',
    loadChildren: () =>
      import('../client-add/client-add.module').then(
        (m) => m.ClientAddPageModule
      ),
  },
  {
    path: 'feedback',
    loadChildren: () =>
      import('../client-feedback/client-feedback.module').then(
        (m) => m.ClientFeedbackPageModule
      ),
  },
  {
    path: ':id',
    loadChildren: () =>
      import('../client-detail/client-detail.module').then(
        (m) => m.ClientDetailPageModule
      ),
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class ClientShellRoutingModule {}

Now let's add the FAB button to the client dashboard page:

<ion-content class="ion-padding">
  <ng-container *ngIf="{ clients: clientsStore.clients$ | async } as vm">
    <app-client-list [clients]="vm.clients"></app-client-list>
  </ng-container>
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
    <ion-fab-button
      data-test="view-feedback-button"
      class="feedback-button"
      routerLink="feedback"
      routerDirection="forward"
    >
      <ion-icon name="chatbubbles-outline"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

Now let's see where we are with the E2E test:

Timed out retrying after 4000ms: Expected to find element: [data-test=list] ion-item, but never found it.
PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).