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
  • Smart view definition

Was this helpful?

  1. How to's
  2. Smart views

Create a custom moderation view

PreviousCreate a custom tinder-like validation viewNextCreate a dynamic calendar view for an event-booking use case

Last updated 3 years ago

Was this helpful?

This example shows you how you can implement a moderations view with a custom Approve/Reject workflow.

In our example, we want to Approve or Reject products to moderate content on our website:

  • We want to preview products images

  • We want to bulk Approve/Reject products

How it works

Smart view definition

This file contains the HTML and CSS needed to build the view.

<div class="view-wrapper">
  <div class="table-wrapper">
    <table class="c-table-frame">
      <thead class="l-table-frame-headers">
        <tr class="l-table-frame-headers-line">
          <th scope="col" role="button" class="c-table-frame__header c-table-frame__header--select-all">
            <div class="c-table-frame__checkbox-select-all">
              <div class="l-table-frame-checkbox-select-all">
                <BetaCheckbox
                  @value={{this.allSelected}}
                  @small={{true}}
                  @disabled={{false}}
                  @onChange={{fn this.selectAll}}
                />
              </div>
            </div>
          </th>
          <th scope="col" class="c-table-frame__header c-table-column-header">
            <span class="c-table-column-header__content">
              <span class="c-table-column-header__display-name
                c-table-column-header__display-name--sortable 
                c-table-column-header__display-name--first" role="button">
                Product details
              </span>
            </span>
          </th>
          <th scope="col" class="c-table-frame__header c-table-column-header">
            <span class="c-table-column-header__content">
              <span class="c-table-column-header__display-name" role="button">
                Images
              </span>
            </span>
          </th>
          <th scope="col" class="c-table-frame__header c-table-column-header">
            <span class="c-table-column-header__content">
              <span class="c-table-column-header__display-name" role="button">
                <Button::BetaButton
                  @type="primary"
                  @text="Approve"
                  @size="tiny"
                  @action={{fn this.triggerSmartAction @collection 'Approve' this.selectedRecords}}
                  @disabled={{this.disableButtons}}
                  @class="no-margin"
                />            
                <Button::BetaButton
                  @type="danger"
                  @text="Reject"
                  @size="tiny"
                  @disabled={{this.disableButtons}}
                  @action={{fn this.triggerSmartAction @collection 'Reject' this.selectedRecords}}
                  @class="no-margin"
                />
              </span>
            </span>
          </th>
        </tr>
      </thead>
      <tbody class="l-table-frame-body">
        {{#each this.formatedRecords as |record|}}
          <tr>
            <td class="align-top first-column" role="">
              <BetaCheckbox
                @value={{record._selected}}
                @small={{true}}
                @disabled={{false}}
                @onChange={{fn this.selectRecord}}
              />
            </td>
            <td class="align-top">
              <div class="title">
                <LinkTo
                  @route="project.rendering.data.collection.list.viewEdit.details"
                  @models={{array @collection.id record.id}}
                >
                 {{record.forest-name}}
                </LinkTo>
              </div>
              <div class="status">
                <span class="c-badge" style="--badge-color:#0cc251; --badge-background-color:#0cc25133;">
                  <p class="c-badge__label">
                    {{record.forest-state}}
                  </p>
                </span>
              </div>
            </td>
            <td class="second-column">
              {{#each record.forest-imagesSF as |image|}}
                <Widgets::Display::FileViewer::WidgetLayout
                  @value={{image}}
                  @field={{this.pictureField}}
                />
              {{/each}}
            </td>
          </tr>
        {{/each}}
      </tbody>
    </table>
  </div>
    
  <Table::TableFooter
    @collection={{@collection}}
    @records={{@records}}
    @selectedRecordsCount={{this.selectedRecords.length}}
    @recordsCount={{@recordsCount}}
    @currentPage={{@currentPage}}
    @numberOfPages={{@numberOfPages}}
    @fetchRecords={{@fetchRecords}}
    @canEdit={{@canEdit}}
    @isLoading={{@isLoading}}
    @displaySearchExtendedButton={{@displaySearchExtendedButton}}
    @disablePagination={{false}}
    @hasShowMore={{false}}
    @class="pagination"
  />
</div>
import Component from '@glimmer/component';
import { triggerSmartAction, deleteRecords } from 'client/utils/smart-view-utils';
import { action, computed } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class GalleryView extends Component {
  @tracked allSelected = false;
  
  constructor(...args) {
    super(...args);
    this.pictureField = this.args.collection.fields.find((f) => {
      return f.fieldName === 'imagesSF'
    });
  }
  @action
  triggerSmartAction(...args) {
    return triggerSmartAction(this, ...args);
  }

  @action
  deleteRecords(...args) {
    return deleteRecords(this, ...args);
  }
  
  @computed('args.records', 'args.records.@each._selected')
  get formatedRecords() {
    if (!this.args.records) return [];
    
    return this.args.records.map((r) => { r._selected = r._selected || false; return r; })
  }
  
  @action
  selectRecord(selected) {
    if (!selected) {
      this.allSelected = false;
    }
    
    if (selected && !this.formatedRecords.find((r) => !r._selected)) {
      this.allSelected = true;
    }
  }
  
  @action
  selectAll(selected) {
    this.formatedRecords.forEach((r) => { r.set('_selected', selected);})
  }
  
  @computed('formatedRecords')
  get selectedRecords() {
    return this.formatedRecords.filter((r) => r._selected);
  }
  
  @computed('selectedRecords')
  get disableButtons() {
    return !this.selectedRecords.length;
  }
  
  willDestroy() {
    this.args.records.forEach((r) => delete r._selected);
  }
}
tr:not(tr:first-child) {
  box-shadow: 0px -17px 0px -16px rgba(176,170,176,1);
}

td.align-top {
  vertical-align: top;
  text-align: center;
}

td.first-column {
  height: 100%;
  padding: 13px 0 0 16px;
}

td.second-column {
  display: flex;
  flex-direction: row;
  flex-grow: 1;
  flex-wrap: wrap;
}

.l-file-viewer-item {
  padding: 8px !important;
  max-width: 80px;
}

.l-file-viewer-item .c-file-viewer__image {
  margin-top: 0;
}

td .title, td .status {
  text-align: center;
  margin-top: 10px;
}

.view-wrapper {
  display: flex;
  flex-direction: column;
  flex-grow: 1;
}

.no-margin {
  margin: 0;
}

.pagination {
  bottom: 0;
  position: sticky;
}

.table-wrapper {
  height: calc(100% - 40px);
}

Learn more about . File template.hbs

smart views