Woodshop for old agent generation
Try the new agent generation
  • What is woodshop
  • How to's
    • Smart Relationship
      • GetIdsFromRequest
    • Smart views
      • Display a calendar view
      • Create a custom tinder-like validation view
      • Create a custom moderation view
      • Create a dynamic calendar view for an event-booking use case
    • Configure environment variables
      • NodeJS/Express projects
    • Elasticsearch Integration
      • Interact with your Elasticsearch data
      • Elasticsearch service/utils
      • Another example
    • Zendesk Integration
      • Authentication, Filtering & Sorting
      • Display Zendesk tickets
      • Display Zendesk users
      • View tickets related to a user
      • Bonus: Direct link to Zendesk + change priority of a ticket
    • Dwolla integration
      • Display Dwolla customers
      • Display Dwolla funding sources
      • Display Dwolla transfers
      • Link users and Dwolla customers
      • Dwolla service
    • Make filters case insensitive
    • Use Azure Table Storage
    • Create multiple line charts
    • Create Charts with AWS Redshift
    • View soft-deleted records
    • Send Smart Action notifications to Slack
    • Authenticate a Forest Admin API against an OAuth protected API Backend
    • Translate your project into TypeScript
      • V8
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v7
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v6
    • Geocode an address with Algolia
    • Display/edit a nested document
    • Send an SMS with Zapier
    • Hash a password with bcrypt
    • Display a customized response
    • Search on a smart field with two joints
    • Override the count route
    • Make a field readOnly with Sequelize
    • Hubspot integration
      • Create a Hubspot company
      • Display Hubspot companies
    • Impersonate a user
    • Import data from a CSV file
    • Import data from a JSON file
    • Load smart fields using hook
    • Pre-fill a form with data from a relationship
    • Re-use a smart field logic
    • Link to record info in a smart view
    • Display data in html format
    • Upload files to AWS S3
    • Display AWS S3 files from signed URLs
    • Prevent record update
    • Display, search and update attributes from a JSON field
    • Add many existing records at the same time (hasMany-belongsTo relationship)
    • Track users’ logs with morgan
    • Search on relationship fields by default
    • Export related data as CSV
    • Run automated tests
  • Forest Admin Documentation
Powered by GitBook
On this page
  • How it works
  • Models definition
  • Smart view definition

Was this helpful?

  1. How to's
  2. Smart views

Create a custom tinder-like validation view

PreviousDisplay a calendar viewNextCreate a custom moderation view

Last updated 4 years ago

Was this helpful?

This example shows you how you can implement a time-saving profile validation view using keyboard keys to trigger approve/reject actions.

In our example, we want to Approve or Reject new customers profiles and more specifically:

  • We want to preview informations from the user's profile

  • We want to approve a customer by pressing the ArrowRight key

  • We want to reject a customer by pressing the ArrowLeft key

How it works

Models definition

Here is the definition of the underlying model for this view

module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  const customerValidations = sequelize.define('customerValidations', {
    firstname: {
      type: DataTypes.STRING,
    },
    lastname: {
      type: DataTypes.STRING,
    },
    email: {
      type: DataTypes.STRING,
    },
    createdAt: {
      type: DataTypes.DATE,
    },
    status: {
      type: DataTypes.STRING,
    },
    avatar: {
      type: DataTypes.STRING,
    },
  }, {
    tableName: 'customers',
    underscored: true,
    schema: process.env.DATABASE_SCHEMA,
  });


  return customerValidations;
};

Smart view definition

Learn more about smart views. This file contains the HTML, JS and CSS needed to build the view.

<div class="c-smart-view">
  <div class="c-smart-view__content">
    {{#if (eq @recordsCount 0)}}
      <span class="c-smart-view_icon fa fa-{{@collection.iconOrDefault}} fa-5x"></span>
      <h1>
        {{@collection.pluralizedDisplayName}}
      </h1>
      <p>
        There are no items to process.
      </p>
    {{/if}}
    {{#unless (eq @recordsCount 0)}}
      <div class="wrapper-view" {{did-insert this.setDefaultCurrentRecord}}>
        <div class="wrapper-list">
          {{#each @records as |record|}}
              <div class="list--item align-left {{if (eq @records.firstObject record) 'selected'}}">
                <div class="list--item__values">
                  <h3><span>name :</span> {{record.forest-firstname}} {{record.forest-lastname}}</h3>
                  <p><span>email :</span> {{record.forest-email}}</p>
                  <p>{{moment-format record.forest-createdAt 'LLL'}}</p>
                </div>
              </div>
          {{/each}}
        </div>
        <div class="wrapper-content">
         <h1>
            {{@recordsCount}} items to process
          </h1>
          <p>
            Press <i class="fa fa-arrow-right"></i> to approve
          </p>
          <p>        
            Press <i class="fa fa-arrow-left"></i> to reject
          </p>
          <div class="record-container" >
            <div class="c-beta-label c-beta-label--top ember-view l-dmb">
              <div class= "c-beta-label__label">Name</div>
              <p class="c-row-value align-left">{{@records.firstObject.forest-firstname}} {{@records.firstObject.forest-lastname}}</p>
            </div>
            <div class="c-beta-label c-beta-label--top ember-view l-dmb">
              <div class= "c-beta-label__label">Email</div>
              <p class="c-row-value align-left">{{@records.firstObject.forest-email}}</p>
            </div>
            <div class= "row-value-image">
              <img src="{{@records.firstObject.forest-avatar}}" width="300" height="400">
            </div>
            <div class="c-beta-button c-beta-button--secondary" onclick={{ action 'triggerSmartAction' @collection 'reject' @records.firstObject}}>Reject</div>
            <div class="c-beta-button c-beta-button--primary" onclick={{ action 'triggerSmartAction' @collection 'approve' @records.firstObject}}>Approve</div>
          </div>
        </div>
      </div>
    {{/unless}}
  </div>
</div>
import Component from '@glimmer/component';
import { action, computed } from '@ember/object';
import { triggerSmartAction, deleteRecords, getCollectionId, loadExternalStyle, loadExternalJavascript } from 'client/utils/smart-view-utils';

export default class TinderView extends Component {

  constructor(...args) {
    super(...args);
    this.eventListener = this._eventListener.bind(this);
    document.addEventListener("keydown", this.eventListener);
  }
  
  @action
  triggerSmartAction(...args) {
    return triggerSmartAction(this, ...args);
  }
  
  _eventListener(event) {
    const keyCode = event.key;
    if (['ArrowLeft', 'ArrowRight'].includes(keyCode)) {
      event.stopPropagation();
      event.preventDefault();
      this.triggerSmartAction(this.args.collection, (keyCode === 'ArrowLeft') ? 'reject' : 'approve', this.args.records.firstObject);
    }
  }
  
  willDestroy() {
    document.removeEventListener('keydown', this.eventListener);
  }
}
.c-smart-view {
  display: flex;
  white-space: normal;
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  top: 0;
  background-color: var(--color-beta-surface);
}

.c-smart-view__content {
  margin: auto;
  text-align: center;
  color: var(--color-beta-on-surface_medium);
}

.c-smart-view_icon {
  margin-bottom: 32px;
}

.record-container {
  border-color: var(--color-beta-on-surface_border);
  border-style: solid;
  border-width: 1px;
  padding: 40px;
  margin: 20px 0px;
  background-color: var(--color-beta-surface-accent);
}

.row-value-image {
  margin-bottom: 15px;
  border-color: var(--color-beta-on-surface_border);
  border-style: solid;
  border-width: 1px;
}

.align-left {
  text-align: left;
}

.wrapper-view {
  display: flex;
  width: 100vw;
  height:100vh;
}

.wrapper-list {
  min-width: 330px;
  border: 1px solid var(--color-beta-on-surface_border);
  border-left: 0px;
  overflow: scroll;
}

.wrapper-content {
  margin-left: 15vw;
  padding:50px;
  height:100vh;
}

.selected {
  border-left: 8px solid var(--color-beta-on-surface_border);
  background-color: var(--color-surface-accent);
  padding-right: Opx;
}

.list--item {
  border-bottom: 1px solid #D9DDE1;
  padding: 14px 30px;
}
  
.list--item__values h3 {
  font-size: 14px;
  font-weight: bold;
  padding: 2px 0;
}
.list--item__values h3 span {
  font-size: 10px;
  text-transform: uppercase;
}
.list--item__values p {
  font-size: 10px;
  padding: 2px 0;
  }
.list--item__values p span {
  text-transform: uppercase;
}