Woodshop
Search…
Create a custom moderation view
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

Learn more about smart views. File template.hbs
This file contains the HTML and CSS needed to build the view.
Template
Component
Style
1
<div class="view-wrapper">
2
<div class="table-wrapper">
3
<table class="c-table-frame">
4
<thead class="l-table-frame-headers">
5
<tr class="l-table-frame-headers-line">
6
<th scope="col" role="button" class="c-table-frame__header c-table-frame__header--select-all">
7
<div class="c-table-frame__checkbox-select-all">
8
<div class="l-table-frame-checkbox-select-all">
9
<BetaCheckbox
10
@value={{this.allSelected}}
11
@small={{true}}
12
@disabled={{false}}
13
@onChange={{fn this.selectAll}}
14
/>
15
</div>
16
</div>
17
</th>
18
<th scope="col" class="c-table-frame__header c-table-column-header">
19
<span class="c-table-column-header__content">
20
<span class="c-table-column-header__display-name
21
c-table-column-header__display-name--sortable
22
c-table-column-header__display-name--first" role="button">
23
Product details
24
</span>
25
</span>
26
</th>
27
<th scope="col" class="c-table-frame__header c-table-column-header">
28
<span class="c-table-column-header__content">
29
<span class="c-table-column-header__display-name" role="button">
30
Images
31
</span>
32
</span>
33
</th>
34
<th scope="col" class="c-table-frame__header c-table-column-header">
35
<span class="c-table-column-header__content">
36
<span class="c-table-column-header__display-name" role="button">
37
<Button::BetaButton
38
@type="primary"
39
@text="Approve"
40
@size="tiny"
41
@action={{fn this.triggerSmartAction @collection 'Approve' this.selectedRecords}}
42
@disabled={{this.disableButtons}}
43
@class="no-margin"
44
/>
45
<Button::BetaButton
46
@type="danger"
47
@text="Reject"
48
@size="tiny"
49
@disabled={{this.disableButtons}}
50
@action={{fn this.triggerSmartAction @collection 'Reject' this.selectedRecords}}
51
@class="no-margin"
52
/>
53
</span>
54
</span>
55
</th>
56
</tr>
57
</thead>
58
<tbody class="l-table-frame-body">
59
{{#each this.formatedRecords as |record|}}
60
<tr>
61
<td class="align-top first-column" role="">
62
<BetaCheckbox
63
@value={{record._selected}}
64
@small={{true}}
65
@disabled={{false}}
66
@onChange={{fn this.selectRecord}}
67
/>
68
</td>
69
<td class="align-top">
70
<div class="title">
71
<LinkTo
72
@route="project.rendering.data.collection.list.viewEdit.details"
73
@models={{array @collection.id record.id}}
74
>
75
{{record.forest-name}}
76
</LinkTo>
77
</div>
78
<div class="status">
79
<span class="c-badge" style="--badge-color:#0cc251; --badge-background-color:#0cc25133;">
80
<p class="c-badge__label">
81
{{record.forest-state}}
82
</p>
83
</span>
84
</div>
85
</td>
86
<td class="second-column">
87
{{#each record.forest-imagesSF as |image|}}
88
<Widgets::Display::FileViewer::WidgetLayout
89
@value={{image}}
90
@field={{this.pictureField}}
91
/>
92
{{/each}}
93
</td>
94
</tr>
95
{{/each}}
96
</tbody>
97
</table>
98
</div>
99
100
<Table::TableFooter
101
@collection={{@collection}}
102
@records={{@records}}
103
@selectedRecordsCount={{this.selectedRecords.length}}
104
@recordsCount={{@recordsCount}}
105
@currentPage={{@currentPage}}
106
@numberOfPages={{@numberOfPages}}
107
@fetchRecords={{@fetchRecords}}
108
@canEdit={{@canEdit}}
109
@isLoading={{@isLoading}}
110
@displaySearchExtendedButton={{@displaySearchExtendedButton}}
111
@disablePagination={{false}}
112
@hasShowMore={{false}}
113
@class="pagination"
114
/>
115
</div>
Copied!
1
import Component from '@glimmer/component';
2
import { triggerSmartAction, deleteRecords } from 'client/utils/smart-view-utils';
3
import { action, computed } from '@ember/object';
4
import { tracked } from '@glimmer/tracking';
5
6
export default class GalleryView extends Component {
7
@tracked allSelected = false;
8
9
constructor(...args) {
10
super(...args);
11
this.pictureField = this.args.collection.fields.find((f) => {
12
return f.fieldName === 'imagesSF'
13
});
14
}
15
@action
16
triggerSmartAction(...args) {
17
return triggerSmartAction(this, ...args);
18
}
19
20
@action
21
deleteRecords(...args) {
22
return deleteRecords(this, ...args);
23
}
24
25
@computed('args.records', '[email protected]_selected')
26
get formatedRecords() {
27
if (!this.args.records) return [];
28
29
return this.args.records.map((r) => { r._selected = r._selected || false; return r; })
30
}
31
32
@action
33
selectRecord(selected) {
34
if (!selected) {
35
this.allSelected = false;
36
}
37
38
if (selected && !this.formatedRecords.find((r) => !r._selected)) {
39
this.allSelected = true;
40
}
41
}
42
43
@action
44
selectAll(selected) {
45
this.formatedRecords.forEach((r) => { r.set('_selected', selected);})
46
}
47
48
@computed('formatedRecords')
49
get selectedRecords() {
50
return this.formatedRecords.filter((r) => r._selected);
51
}
52
53
@computed('selectedRecords')
54
get disableButtons() {
55
return !this.selectedRecords.length;
56
}
57
58
willDestroy() {
59
this.args.records.forEach((r) => delete r._selected);
60
}
61
}
Copied!
1
tr:not(tr:first-child) {
2
box-shadow: 0px -17px 0px -16px rgba(176,170,176,1);
3
}
4
5
td.align-top {
6
vertical-align: top;
7
text-align: center;
8
}
9
10
td.first-column {
11
height: 100%;
12
padding: 13px 0 0 16px;
13
}
14
15
td.second-column {
16
display: flex;
17
flex-direction: row;
18
flex-grow: 1;
19
flex-wrap: wrap;
20
}
21
22
.l-file-viewer-item {
23
padding: 8px !important;
24
max-width: 80px;
25
}
26
27
.l-file-viewer-item .c-file-viewer__image {
28
margin-top: 0;
29
}
30
31
td .title, td .status {
32
text-align: center;
33
margin-top: 10px;
34
}
35
36
.view-wrapper {
37
display: flex;
38
flex-direction: column;
39
flex-grow: 1;
40
}
41
42
.no-margin {
43
margin: 0;
44
}
45
46
.pagination {
47
bottom: 0;
48
position: sticky;
49
}
50
51
.table-wrapper {
52
height: calc(100% - 40px);
53
}
Copied!
Last modified 2mo ago