Create a custom moderation view
PreviousCreate a custom tinder-like validation viewNextCreate a dynamic calendar view for an event-booking use case
Last updated
Last updated
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
Learn more about smart views. File template.hbs
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);
}