@bennerinformatics/ember-fw-table 2.0.19 → 2.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/addon/classes/Row.js +74 -74
- package/addon/classes/Table.js +147 -147
- package/addon/components/fw-cell-action.js +15 -15
- package/addon/components/fw-cell-boolean.js +12 -12
- package/addon/components/fw-cell-nullable.js +12 -12
- package/addon/components/fw-cell-permission-icon.js +12 -12
- package/addon/components/fw-column-title.js +13 -13
- package/addon/components/fw-delete-modal.js +61 -61
- package/addon/components/fw-pagination-wrapper.js +681 -681
- package/addon/components/fw-row-toggle-index.js +14 -14
- package/addon/components/fw-table-expanded-row.js +24 -24
- package/addon/components/fw-table-resort.js +3 -3
- package/addon/components/fw-table-sortable.js +398 -389
- package/addon/documentation.js +98 -98
- package/addon/templates/components/fw-delete-modal.hbs +2 -7
- package/addon/templates/components/fw-pagination-wrapper.hbs +45 -74
- package/addon/templates/components/fw-table-expanded-row.hbs +23 -23
- package/addon/templates/components/fw-table-expanded-rows.hbs +1 -1
- package/addon/templates/components/fw-table-resort.hbs +4 -4
- package/addon/templates/components/fw-table-sortable.hbs +9 -31
- package/addon/utils/base-cells.js +40 -40
- package/addon/utils/export.js +76 -76
- package/addon/utils/formats.js +46 -46
- package/addon/utils/table.js +35 -35
- package/app/breakpoints.js +1 -1
- package/app/components/fw-cell-permission-icon.js +1 -1
- package/app/initializers/responsive.js +1 -1
- package/bitbucket-helpers-override.js +240 -240
- package/codemods.log +16 -0
- package/index.js +9 -10
- package/package.json +67 -67
- package/yuidoc.json +21 -21
|
@@ -1,681 +1,681 @@
|
|
|
1
|
-
import Component from '@ember/component';
|
|
2
|
-
import {computed} from '@ember/object';
|
|
3
|
-
import {empty, filterBy} from '@ember/object/computed';
|
|
4
|
-
import {inject} from '@ember/service';
|
|
5
|
-
import {isEmpty, isNone} from '@ember/utils';
|
|
6
|
-
import {handleAjaxError} from '@bennerinformatics/ember-fw/utils/error';
|
|
7
|
-
import exportTable from '@bennerinformatics/ember-fw-table/utils/export';
|
|
8
|
-
import Table from 'ember-light-table';
|
|
9
|
-
import RSVP from 'rsvp';
|
|
10
|
-
import layout from '../templates/components/fw-pagination-wrapper';
|
|
11
|
-
/**
|
|
12
|
-
* In order to use this component, you also need to install the `ember-cli-pagination` addon
|
|
13
|
-
* to your app. And to make sure that the css looks correct for the pages, in your app.scss file
|
|
14
|
-
* import the styling after all related `ember-fw` imports:
|
|
15
|
-
* ```css
|
|
16
|
-
* @import "ember-fw/pagination/pagination-fw";
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* This component works very closely with our Informatics Framework system to create a
|
|
20
|
-
* paginated table, but being such, it also expects certain things from the browse request
|
|
21
|
-
* to get it to work. So that being said, this documentation will need to describe how to use
|
|
22
|
-
* both the client side and the serverside of this component. If you do not use our FW system as
|
|
23
|
-
* your server side, you will need to find a way to get it to return count and limited as is detailed here.
|
|
24
|
-
*
|
|
25
|
-
* ###Client Side
|
|
26
|
-
* There are a few things that are needed to be talked about with the client side. There are some parameters
|
|
27
|
-
* that are needed no matter whether you wrap a table or use a default table. But when it comes to the table,
|
|
28
|
-
* you can use block form, where you specify the table (see [fw-table-sortable](FW-Table-Sortable.html)),
|
|
29
|
-
* or you can pass in extra parameters so this function, and it will make a table for you.
|
|
30
|
-
*
|
|
31
|
-
* ####Mandatory Parameters
|
|
32
|
-
* While below deals with all of the different options that you can pass in as properties to this parameter, as
|
|
33
|
-
* well as all internal properties used by the component by way of explanation, referencing those parameters that
|
|
34
|
-
* are necessary for the proper functioning of this component are helpful here. See below for details about each property:
|
|
35
|
-
*
|
|
36
|
-
* * Basic Properties
|
|
37
|
-
* - `modelName`
|
|
38
|
-
* - `defaultSortKey`
|
|
39
|
-
* - `tableWrapperClass`
|
|
40
|
-
* - `entriesPerPage`
|
|
41
|
-
* * Action Properties (these need to be actions that are passed in)
|
|
42
|
-
* - `onSearch`
|
|
43
|
-
* - `makeQuery`
|
|
44
|
-
* - `getTitle`
|
|
45
|
-
* - `getExportColumns`
|
|
46
|
-
*
|
|
47
|
-
* ####Setting Up the Table
|
|
48
|
-
* #####Table Column Usage
|
|
49
|
-
* The following parameters from table columns are utilized in searching and sorting.
|
|
50
|
-
* searchKey: Key to send to the server side when sorting by this column. If unset, defaults to valuePath. Most commonly used when valuePath uses a relationship property, as searchKey can then just use the relationship name.
|
|
51
|
-
* component: To show the loading spinner on the header during searching, component must be set to a header component that shows a spinner when column.loading is true. fw-column-title is an option that will handle this for you.
|
|
52
|
-
*
|
|
53
|
-
* #####Block invoking
|
|
54
|
-
* This template is typically invoked in block format, wrapping around the history search panel. When called in block form, a single hash parameter called actions is provided. This contains the following properties:
|
|
55
|
-
* actions.search: Action to call to use search parameter and fetch entries.
|
|
56
|
-
* actions.export: Action to call to fetch all entries and export them into a CSV file.
|
|
57
|
-
*
|
|
58
|
-
* These actions should be used within the search panel to create search and export buttons.
|
|
59
|
-
* Table
|
|
60
|
-
* The results table can be provided in two forms: block format and parameter format.
|
|
61
|
-
* Parameter format
|
|
62
|
-
* Calling the table in parameter format will use a default fw-table-sortable. In this format, the following additional parameters to fw-pagination-wrapper are available:
|
|
63
|
-
* getTableColumns: Action callback to get the table columns. Callback in case it changes on search, but has no parameters
|
|
64
|
-
* emptyText: Text to display when the table is empty. If unset, hides the table when empty.
|
|
65
|
-
* tableActions: Parameter to send into fw-table-sortable’s tableActions
|
|
66
|
-
* Block format
|
|
67
|
-
* Block format allows passing in a custom table component instead of using fw-table-sortable. In this format, the table is included in the block with the search panel. To distinguish the two, a second parameter, table, is included. It will be an object for the table, and null for the search panel. The code below shows an example of using the block format with both parameters:
|
|
68
|
-
* ```hbs
|
|
69
|
-
*
|
|
70
|
-
* …
|
|
71
|
-
* as |actions table|
|
|
72
|
-
*
|
|
73
|
-
* {{#unless table}}
|
|
74
|
-
* {{!-- Search panel contents --}}
|
|
75
|
-
* {{else}}
|
|
76
|
-
* {{!-- Table component invocation --}}
|
|
77
|
-
* {{/unless}}
|
|
78
|
-
*
|
|
79
|
-
* ```
|
|
80
|
-
* In block format when table is defined, actions contains the following actions:
|
|
81
|
-
* actions.sort: Action to use in fw-table-sortable’s onSort, called when a column is clicked.
|
|
82
|
-
*
|
|
83
|
-
* table contains the following parameters:
|
|
84
|
-
* table.title: Full title for the table, pass to tables as title.
|
|
85
|
-
* table.suffix: Suffix for the table title. Used in some custom table components.
|
|
86
|
-
* table.entries: List of entries to display in the table. Passed to a table as the first unnamed parameter, rows.
|
|
87
|
-
* table.sortKey: Currently active sort key for the table. Passed to a table as defaultSort *
|
|
88
|
-
* ###Server Side
|
|
89
|
-
*
|
|
90
|
-
* The client side performs all history behavior using the browse method of the given model, based on the modelName parameter. The browse route must support several query parameters to handle all cases.
|
|
91
|
-
* Count
|
|
92
|
-
* The count parameter is a boolean that when set to true returns a count of results instead of the results. This is needed to calculate the number of pages available and to show the total in the table title.
|
|
93
|
-
*
|
|
94
|
-
* Count can be performed simply using $this->adapter->count in place of findAll. It takes two parameters, $modelName and $query, same as the first two parameters to findAll. For usage in the table, the result must be placed in a key, “count”, such as with the following code:
|
|
95
|
-
* $count = $this->adapter->count($modelName, $query);
|
|
96
|
-
* return $this->view->helper('json')->add($count, 'count');
|
|
97
|
-
* Limit and Offset
|
|
98
|
-
* The main feature of pagination is the ability to fetch only one page of results at a time, reducing the amount of data fetched in requests.
|
|
99
|
-
* limit:
|
|
100
|
-
* This parameter determines the number of entries per page, as defined by entriesPerPage.
|
|
101
|
-
* This can be accomplished using a filter with $qb->limit.
|
|
102
|
-
* offset:
|
|
103
|
-
* This determines the first entry to be fetched for the limit.
|
|
104
|
-
* This can be accomplished using a filter with $qb->offset.
|
|
105
|
-
*
|
|
106
|
-
* The following code implements both limit and offset:
|
|
107
|
-
* ```js
|
|
108
|
-
* if (isset($options->limit)) {* $limit = $options->limit;* $offset = $options->offset ?? 0;* $this->adapter->addFilter($modelName, function($qb)* use ($limit, $offset) {* $qb->limit($limit);* $qb->offset($offset);* });
|
|
109
|
-
* }
|
|
110
|
-
* ```
|
|
111
|
-
* Sorting
|
|
112
|
-
* Pagination requires refetching all pages every time the sort order changes, as the first result may not be on the first page. fw-pagination-wrapper handles all the logic needed to do that client side, but it needs to be supported on the server side to work. This requires two parameters:
|
|
113
|
-
* sortKey: Table key to use in sorting
|
|
114
|
-
* ascending: If true, sorts results ascending. If false, sorts them descending.
|
|
115
|
-
*
|
|
116
|
-
* The following code implements sort key and ascending:
|
|
117
|
-
* ```js
|
|
118
|
-
* if (isset($options->sortKey)) {* $sortKey = alias($options->sortKey);* $ascending = ($options->ascending ?? 'false') == 'true';* $ascending = $ascending ? 'ASC' : 'DESC';* $this->adapter->addFilter($modelName, function($qb)* use ($sortKey, $ascending) {* $qb->orderBy($sortKey, $ascending);
|
|
119
|
-
* });
|
|
120
|
-
* }
|
|
121
|
-
* ```
|
|
122
|
-
* In this code, alias is a function that maps the sortKey from the client side name used in the parameter to the server side name supported by SQL. Point of Sales handles this using a function in the model, called using $this->adapter->aliasKey.
|
|
123
|
-
*
|
|
124
|
-
* Note you may need more complex code to sort by complex table fields, such as a relationship (such as Status’s location name) or a computed value (such as Point of Sales’s transaction log total). An example of this can be found in Point of Sales.
|
|
125
|
-
* Design Considerations
|
|
126
|
-
* It is important to note that if you want to use SQL limiting and offsets, you should do all filtering and sorting using SQL. This means that any filters in PHP need to be migrated to SQL or otherwise some pages will return too few results.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* @class FW-Pagination-Wrapper
|
|
130
|
-
*/
|
|
131
|
-
export default Component.extend({
|
|
132
|
-
layout,
|
|
133
|
-
ajax: inject(),
|
|
134
|
-
config: inject(),
|
|
135
|
-
media: inject(),
|
|
136
|
-
notifications: inject(),
|
|
137
|
-
store: inject(),
|
|
138
|
-
tagName: '',
|
|
139
|
-
|
|
140
|
-
didReceiveAttrs() {
|
|
141
|
-
this._super(...arguments);
|
|
142
|
-
if (this.get('searchOnRender')) {
|
|
143
|
-
this.send('search');
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
|
|
147
|
-
/*
|
|
148
|
-
* Parameters
|
|
149
|
-
*/
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Name of the model in search
|
|
153
|
-
* @type {String}
|
|
154
|
-
* @property modelName
|
|
155
|
-
*/
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Whether or not the table loads on page load. If true, it will call the search based on your defaults already set, whenever the table is rendered for the first time.
|
|
159
|
-
* @type {Boolean}
|
|
160
|
-
* @property searchOnRender
|
|
161
|
-
* @default false
|
|
162
|
-
*/
|
|
163
|
-
searchOnRender: false,
|
|
164
|
-
/**
|
|
165
|
-
* Default sort order for the table
|
|
166
|
-
* @type {String}
|
|
167
|
-
* @property defaultSortKey
|
|
168
|
-
*/
|
|
169
|
-
defaultSortKey: null,
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Number of entries per page
|
|
173
|
-
* @type {Number}
|
|
174
|
-
* @property entriesPerPage
|
|
175
|
-
* @default 100
|
|
176
|
-
*/
|
|
177
|
-
entriesPerPage: 100,
|
|
178
|
-
/**
|
|
179
|
-
* Show the page numbers above the table. If false, they won't show.
|
|
180
|
-
* @type {Boolean}
|
|
181
|
-
* @property showPagesTop
|
|
182
|
-
* @default true
|
|
183
|
-
*/
|
|
184
|
-
showPagesTop: true,
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Show the page numbers below the table. If false, they won't show.
|
|
188
|
-
* @type {Boolean}
|
|
189
|
-
* @property showPagesBottom
|
|
190
|
-
* @default true
|
|
191
|
-
*/
|
|
192
|
-
showPagesBottom: true,
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Gets the title of this table at the given time
|
|
196
|
-
* @property getTitle
|
|
197
|
-
* @type {Action}
|
|
198
|
-
* @return {String} Title of this table
|
|
199
|
-
*/
|
|
200
|
-
getTitle() {
|
|
201
|
-
return 'Table';
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Gets a list of table columns for exporting
|
|
206
|
-
* @property getTableColumns
|
|
207
|
-
* @type {Action}
|
|
208
|
-
* @return {Array} Array of table columns
|
|
209
|
-
*/
|
|
210
|
-
getExportColumns() {},
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Action to be called when the table search button is pressed.
|
|
214
|
-
* @type {Function}
|
|
215
|
-
* @property onSearch
|
|
216
|
-
*/
|
|
217
|
-
onSearch() {},
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Called to delete the full page of entries
|
|
221
|
-
* Should be passed in with a function
|
|
222
|
-
* @property deletePage
|
|
223
|
-
*/
|
|
224
|
-
deletePage: undefined,
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Determines permission for deleteTablePermission for fw-table-sortable
|
|
228
|
-
* @property deletePagePermission
|
|
229
|
-
* @type {Boolean}
|
|
230
|
-
* @default true
|
|
231
|
-
*/
|
|
232
|
-
deletePagePermission: true,
|
|
233
|
-
/**
|
|
234
|
-
* Makes a query object based on the search fields
|
|
235
|
-
* @property makeQuery
|
|
236
|
-
* @type {Action}
|
|
237
|
-
* @param {Boolean} count If true, counting
|
|
238
|
-
* @param {Number} page If defined, page number for a page search
|
|
239
|
-
* @param {Boolean} export If true, exporting
|
|
240
|
-
* @return {Object} Query object
|
|
241
|
-
*/
|
|
242
|
-
makeQuery(/* {count, page, export} */) {
|
|
243
|
-
return {};
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
/* Table generation properties */
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Gets a list of table columns for the results. If null, yields for table
|
|
250
|
-
* @method getTableColumns
|
|
251
|
-
* @return {Array} Array of table columns
|
|
252
|
-
*/
|
|
253
|
-
getTableColumns() {
|
|
254
|
-
return null;
|
|
255
|
-
},
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Text to show for empty tables. If null, hides on empty
|
|
259
|
-
* @property emptyText
|
|
260
|
-
* @type {String}
|
|
261
|
-
*/
|
|
262
|
-
emptyText: null,
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Actions to pass into the table
|
|
266
|
-
* @property tableActions
|
|
267
|
-
* @type {Object}
|
|
268
|
-
*/
|
|
269
|
-
tableActions: null,
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Class names for the wrapper around the loading spinner and the table
|
|
273
|
-
* @property tableWrapperClass
|
|
274
|
-
* @type {String}
|
|
275
|
-
*/
|
|
276
|
-
tableWrapperClass: '',
|
|
277
|
-
|
|
278
|
-
/*
|
|
279
|
-
* Search properties
|
|
280
|
-
*/
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Table title at this time
|
|
284
|
-
* @property currentTitle
|
|
285
|
-
* @type {String}
|
|
286
|
-
*/
|
|
287
|
-
currentTitle: null,
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* If true, currently doing the main search
|
|
291
|
-
* @property searchingTable
|
|
292
|
-
* @type {Boolean}
|
|
293
|
-
* @default false
|
|
294
|
-
*/
|
|
295
|
-
searchingTable: false,
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* If true, show export page button
|
|
299
|
-
* @property showExport
|
|
300
|
-
* @type {Boolean}
|
|
301
|
-
* @default true
|
|
302
|
-
*/
|
|
303
|
-
showExport: true,
|
|
304
|
-
/**
|
|
305
|
-
* Number of pages being searched
|
|
306
|
-
* @property pagesSearching
|
|
307
|
-
* @type {Number}
|
|
308
|
-
* @default 0
|
|
309
|
-
*/
|
|
310
|
-
pagesSearching: 0,
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Array of entries, indexes are the pages
|
|
314
|
-
* @property pageEntries
|
|
315
|
-
* @type {Array}
|
|
316
|
-
*/
|
|
317
|
-
pageEntries: null,
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Currently selected page
|
|
321
|
-
* @property page
|
|
322
|
-
* @type {Number}
|
|
323
|
-
* @default 1
|
|
324
|
-
*/
|
|
325
|
-
page: 1,
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Total number of entries
|
|
329
|
-
* @property count
|
|
330
|
-
* @type {Number}
|
|
331
|
-
*/
|
|
332
|
-
count: null,
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Query last time createQuery was called
|
|
336
|
-
* @property lastQuery
|
|
337
|
-
*/
|
|
338
|
-
lastQuery: null,
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Current sort order for the table
|
|
342
|
-
* @type {String}
|
|
343
|
-
* @property currentSortKey
|
|
344
|
-
*/
|
|
345
|
-
currentSortKey: null,
|
|
346
|
-
|
|
347
|
-
/*
|
|
348
|
-
* Computed properties
|
|
349
|
-
*/
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Array index for the selected page
|
|
353
|
-
* @type {Number - Computed}
|
|
354
|
-
* @property index
|
|
355
|
-
* @internal
|
|
356
|
-
*/
|
|
357
|
-
index: computed('page', function() {
|
|
358
|
-
return this.get('page') - 1;
|
|
359
|
-
}),
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Array of entries at the current page
|
|
363
|
-
* @type {Array - Computed}
|
|
364
|
-
* @property currentEntries
|
|
365
|
-
* @internal
|
|
366
|
-
*/
|
|
367
|
-
currentEntries: computed('pageEntries.[]', 'index', function() {
|
|
368
|
-
let entries = this.get('pageEntries');
|
|
369
|
-
if (isNone(entries)) {
|
|
370
|
-
return [];
|
|
371
|
-
}
|
|
372
|
-
return entries.objectAt(this.get('index'));
|
|
373
|
-
}),
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Filtered entries, removing deleted entries
|
|
377
|
-
* @type {Array - Computed}
|
|
378
|
-
* @property filteredEntries
|
|
379
|
-
* @internal
|
|
380
|
-
*/
|
|
381
|
-
filteredEntries: filterBy('currentEntries', 'isDeleted', false),
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Current table sort key based on given properties
|
|
385
|
-
* @type {String - Computed}
|
|
386
|
-
* @property tableSortKey
|
|
387
|
-
* @internal
|
|
388
|
-
*/
|
|
389
|
-
tableSortKey: computed('defaultSortKey', 'currentSortKey', function() {
|
|
390
|
-
let current = this.get('currentSortKey');
|
|
391
|
-
if (isEmpty(current)) {
|
|
392
|
-
return `${this.get('defaultSortKey')}:desc`;
|
|
393
|
-
}
|
|
394
|
-
return current;
|
|
395
|
-
}),
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Number of pages available
|
|
399
|
-
* @type {Number - Computed}
|
|
400
|
-
* @property totalPages
|
|
401
|
-
* @internal
|
|
402
|
-
*/
|
|
403
|
-
totalPages: computed('count', 'entriesPerPage', function() {
|
|
404
|
-
let count = this.get('count');
|
|
405
|
-
if (isNone(count)) {
|
|
406
|
-
return 0;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return Math.ceil(count / this.get('entriesPerPage'));
|
|
410
|
-
}),
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* True if we have multiple pages
|
|
414
|
-
* @type {Boolean - Computed}
|
|
415
|
-
* @property showPages
|
|
416
|
-
* @internal
|
|
417
|
-
*/
|
|
418
|
-
showPages: computed('totalPages', function() {
|
|
419
|
-
return this.get('totalPages') > 1;
|
|
420
|
-
}),
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Maximum number of pages to show in the pagination component
|
|
424
|
-
* @type {Number - Computed}
|
|
425
|
-
* @property maxPageButtons
|
|
426
|
-
* @internal
|
|
427
|
-
*/
|
|
428
|
-
maxPageButtons: computed('media.{isMobile,isTablet}', function() {
|
|
429
|
-
let media = this.get('media');
|
|
430
|
-
if (media.get('isMobile')) {
|
|
431
|
-
return 3;
|
|
432
|
-
}
|
|
433
|
-
if (media.get('isTablet')) {
|
|
434
|
-
return 5;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return 7;
|
|
438
|
-
}),
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Gets the serverside route to use for this model name
|
|
442
|
-
* @type {String - Computed}
|
|
443
|
-
* @property routeName
|
|
444
|
-
* @internal
|
|
445
|
-
*/
|
|
446
|
-
routeName: computed('modelName', function() {
|
|
447
|
-
let name = this.get('modelName');
|
|
448
|
-
return this.get('store').adapterFor(name).pathForType(name);
|
|
449
|
-
}),
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Final piece of title for table
|
|
453
|
-
* @type {String - Computed}
|
|
454
|
-
* @property tableSuffix
|
|
455
|
-
* @internal
|
|
456
|
-
*/
|
|
457
|
-
tableSuffix: computed('count', 'currentEntries', 'index', function() {
|
|
458
|
-
let count = this.get('count');
|
|
459
|
-
let entries = this.get('currentEntries');
|
|
460
|
-
if (isEmpty(entries)) {
|
|
461
|
-
return `${count} entries`;
|
|
462
|
-
}
|
|
463
|
-
return `${entries.get('length')} of ${count} entries`;
|
|
464
|
-
}),
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Title for the table
|
|
468
|
-
* @type {String - Computed}
|
|
469
|
-
* @property fullTableTitle
|
|
470
|
-
* @internal
|
|
471
|
-
*/
|
|
472
|
-
fullTableTitle: computed('currentTitle', 'tableSuffix', function() {
|
|
473
|
-
return `${this.get('currentTitle')} - ${this.get('tableSuffix')}`;
|
|
474
|
-
}),
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* If true, hide the table when empty
|
|
478
|
-
* @type {Boolean - Computed}
|
|
479
|
-
* @property hideEmpty
|
|
480
|
-
* @internal
|
|
481
|
-
*/
|
|
482
|
-
hideEmpty: empty('emptyText'),
|
|
483
|
-
|
|
484
|
-
/* Functions */
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Queries the serverside to get the total record count
|
|
488
|
-
* @method queryCount
|
|
489
|
-
* @return {Promise} Promise that resolves to a number
|
|
490
|
-
*/
|
|
491
|
-
queryCount() {
|
|
492
|
-
// fetch standard query
|
|
493
|
-
let query = this.makeQuery({count: true});
|
|
494
|
-
query.count = true;
|
|
495
|
-
|
|
496
|
-
// make request
|
|
497
|
-
let url = this.get('config').formUrl(this.get('routeName'));
|
|
498
|
-
return this.get('ajax').request(url, {data: query}).then((({count}) => count)).catch(handleAjaxError.bind(this));
|
|
499
|
-
},
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Query for settign a new sort order
|
|
503
|
-
* @method querySort
|
|
504
|
-
* @param {Number} page Page number to start
|
|
505
|
-
* @param {String} sortKey New sort order
|
|
506
|
-
* @param {Boolean} ascending If true, sorts ascending, false descending
|
|
507
|
-
* @return {Promise} Promise that resolves to a entry array
|
|
508
|
-
*/
|
|
509
|
-
querySort(page, sortKey, ascending) {
|
|
510
|
-
let query = this.get('lastQuery');
|
|
511
|
-
|
|
512
|
-
// set sort key stuff if present
|
|
513
|
-
if (!isNone(ascending)) {
|
|
514
|
-
query.ascending = ascending;
|
|
515
|
-
}
|
|
516
|
-
if (!isEmpty(sortKey)) {
|
|
517
|
-
query.sortKey = sortKey;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// set limits on query
|
|
521
|
-
let entriesPerPage = this.get('entriesPerPage');
|
|
522
|
-
query.limit = entriesPerPage;
|
|
523
|
-
query.offset = (page - 1) * entriesPerPage;
|
|
524
|
-
|
|
525
|
-
// make promise
|
|
526
|
-
return RSVP.resolve(this.get('store').query(this.get('modelName'), query)).catch(handleAjaxError.bind(this));
|
|
527
|
-
},
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Fetches the entries for the given page number
|
|
531
|
-
* @method queryPage
|
|
532
|
-
* @param {Number} page Page to fetch
|
|
533
|
-
* @return {Promise} Promise that resolves to an entry array
|
|
534
|
-
*/
|
|
535
|
-
queryPage(page) {
|
|
536
|
-
// same as sort, but handles the entries
|
|
537
|
-
return this.querySort(page).then((entries) => {
|
|
538
|
-
this.get('pageEntries')[page - 1] = entries;
|
|
539
|
-
return entries;
|
|
540
|
-
});
|
|
541
|
-
},
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Gets all entries for the given query
|
|
545
|
-
* @method queryAll
|
|
546
|
-
* @return {Promise} Promise that resolves to entries
|
|
547
|
-
*/
|
|
548
|
-
queryAll() {
|
|
549
|
-
let query = this.makeQuery({export: true});
|
|
550
|
-
return RSVP.resolve(this.get('store').query(this.get('modelName'), query)).catch(handleAjaxError.bind(this));
|
|
551
|
-
},
|
|
552
|
-
|
|
553
|
-
actions: {
|
|
554
|
-
/* Search buttons */
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* This action is called when the search button is pressed
|
|
558
|
-
* @method search
|
|
559
|
-
* @return {Promise} Promise that resolves after searching
|
|
560
|
-
*/
|
|
561
|
-
search() {
|
|
562
|
-
// TODO: canSearch?
|
|
563
|
-
// else a title getter
|
|
564
|
-
|
|
565
|
-
// start search and clean up old data
|
|
566
|
-
this.setProperties({
|
|
567
|
-
currentTitle: this.getTitle(),
|
|
568
|
-
// search data
|
|
569
|
-
lastQuery: this.makeQuery({page: 1}),
|
|
570
|
-
pageEntries: [],
|
|
571
|
-
page: 1,
|
|
572
|
-
// searching keys
|
|
573
|
-
searchingTable: true,
|
|
574
|
-
pagesSearching: 0,
|
|
575
|
-
tableColumns: this.getTableColumns()
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
// search callback
|
|
579
|
-
this.onSearch();
|
|
580
|
-
|
|
581
|
-
// make two requests: one for the total count and one for the first 100 entries
|
|
582
|
-
return RSVP.hash({
|
|
583
|
-
count: this.queryCount(),
|
|
584
|
-
entries: this.queryPage(1)
|
|
585
|
-
}).then(({count}) => {
|
|
586
|
-
// entries already set as part of queryPage
|
|
587
|
-
this.setProperties({
|
|
588
|
-
count,
|
|
589
|
-
searchingTable: false
|
|
590
|
-
});
|
|
591
|
-
}).catch(() => {
|
|
592
|
-
// request failed, clean up data
|
|
593
|
-
this.setProperties({
|
|
594
|
-
// search data
|
|
595
|
-
pageEntries: null,
|
|
596
|
-
count: 0,
|
|
597
|
-
// searching keys
|
|
598
|
-
searchingTable: false
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
},
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* This action is called when the export button is clicked to export all data
|
|
605
|
-
* @method export
|
|
606
|
-
* @return {Promise} Promise that resolves after exporting the table
|
|
607
|
-
*/
|
|
608
|
-
export() {
|
|
609
|
-
// TODO: canSearch?
|
|
610
|
-
|
|
611
|
-
// build table for export
|
|
612
|
-
let table = new Table(this.getExportColumns());
|
|
613
|
-
return this.queryAll().then((entries) => {
|
|
614
|
-
table.setRows(entries.sortBy(this.get('defaultSortKey')).reverse());
|
|
615
|
-
exportTable(table, `${this.getTitle()} - All Entries`);
|
|
616
|
-
}).catch(handleAjaxError.bind(this));
|
|
617
|
-
},
|
|
618
|
-
|
|
619
|
-
/* Pagination */
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* This action is called when a page button is clicked to switch pages
|
|
623
|
-
* @method setPage
|
|
624
|
-
* @param {Number} page New page number to set
|
|
625
|
-
*/
|
|
626
|
-
setPage(page) {
|
|
627
|
-
// clamp page number
|
|
628
|
-
let max = this.get('totalPages');
|
|
629
|
-
if (page < 1) {
|
|
630
|
-
page = 1;
|
|
631
|
-
} else if (page > max) {
|
|
632
|
-
page = max;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// if we havve entries at the page number, use those
|
|
636
|
-
// if missing, query them
|
|
637
|
-
if (isNone(this.get('pageEntries').objectAt(page - 1))) {
|
|
638
|
-
this.incrementProperty('pagesSearching');
|
|
639
|
-
this.set('page', page);
|
|
640
|
-
this.queryPage(page).then(() => {
|
|
641
|
-
// entries set in promise logic
|
|
642
|
-
this.decrementProperty('pagesSearching');
|
|
643
|
-
});
|
|
644
|
-
} else {
|
|
645
|
-
this.set('page', page);
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
|
|
649
|
-
/* Sorting */
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* This action resorts the entry by the given column
|
|
653
|
-
* @method sortColumn
|
|
654
|
-
* @param {Column} column Column to use for sorting
|
|
655
|
-
* @param {String} sortKey String to use for sorting in the column
|
|
656
|
-
* @return {Promise} Promise that resolves to entries
|
|
657
|
-
*/
|
|
658
|
-
sortColumn(column, sortKey) {
|
|
659
|
-
// if the sort key is unchanged, do nothing
|
|
660
|
-
if (sortKey === this.get('tableSortKey')) {
|
|
661
|
-
return RSVP.resolve();
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// mark that we are sorting, column properties do not add desc
|
|
665
|
-
column.set('sorting', true);
|
|
666
|
-
|
|
667
|
-
// search for data
|
|
668
|
-
let page = this.get('page');
|
|
669
|
-
return this.querySort(page, column.searchKey || column.valuePath, column.ascending).then((entries) => {
|
|
670
|
-
// set entries to new list
|
|
671
|
-
let pageEntries = [];
|
|
672
|
-
pageEntries[page - 1] = entries;
|
|
673
|
-
this.setProperties({pageEntries, currentSortKey: sortKey});
|
|
674
|
-
|
|
675
|
-
// mark that we are done sorting
|
|
676
|
-
column.set('sorting', false);
|
|
677
|
-
return entries;
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
});
|
|
1
|
+
import Component from '@ember/component';
|
|
2
|
+
import {computed} from '@ember/object';
|
|
3
|
+
import {empty, filterBy} from '@ember/object/computed';
|
|
4
|
+
import {inject} from '@ember/service';
|
|
5
|
+
import {isEmpty, isNone} from '@ember/utils';
|
|
6
|
+
import {handleAjaxError} from '@bennerinformatics/ember-fw/utils/error';
|
|
7
|
+
import exportTable from '@bennerinformatics/ember-fw-table/utils/export';
|
|
8
|
+
import Table from 'ember-light-table';
|
|
9
|
+
import RSVP from 'rsvp';
|
|
10
|
+
import layout from '../templates/components/fw-pagination-wrapper';
|
|
11
|
+
/**
|
|
12
|
+
* In order to use this component, you also need to install the `ember-cli-pagination` addon
|
|
13
|
+
* to your app. And to make sure that the css looks correct for the pages, in your app.scss file
|
|
14
|
+
* import the styling after all related `ember-fw` imports:
|
|
15
|
+
* ```css
|
|
16
|
+
* @import "ember-fw/pagination/pagination-fw";
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* This component works very closely with our Informatics Framework system to create a
|
|
20
|
+
* paginated table, but being such, it also expects certain things from the browse request
|
|
21
|
+
* to get it to work. So that being said, this documentation will need to describe how to use
|
|
22
|
+
* both the client side and the serverside of this component. If you do not use our FW system as
|
|
23
|
+
* your server side, you will need to find a way to get it to return count and limited as is detailed here.
|
|
24
|
+
*
|
|
25
|
+
* ###Client Side
|
|
26
|
+
* There are a few things that are needed to be talked about with the client side. There are some parameters
|
|
27
|
+
* that are needed no matter whether you wrap a table or use a default table. But when it comes to the table,
|
|
28
|
+
* you can use block form, where you specify the table (see [fw-table-sortable](FW-Table-Sortable.html)),
|
|
29
|
+
* or you can pass in extra parameters so this function, and it will make a table for you.
|
|
30
|
+
*
|
|
31
|
+
* ####Mandatory Parameters
|
|
32
|
+
* While below deals with all of the different options that you can pass in as properties to this parameter, as
|
|
33
|
+
* well as all internal properties used by the component by way of explanation, referencing those parameters that
|
|
34
|
+
* are necessary for the proper functioning of this component are helpful here. See below for details about each property:
|
|
35
|
+
*
|
|
36
|
+
* * Basic Properties
|
|
37
|
+
* - `modelName`
|
|
38
|
+
* - `defaultSortKey`
|
|
39
|
+
* - `tableWrapperClass`
|
|
40
|
+
* - `entriesPerPage`
|
|
41
|
+
* * Action Properties (these need to be actions that are passed in)
|
|
42
|
+
* - `onSearch`
|
|
43
|
+
* - `makeQuery`
|
|
44
|
+
* - `getTitle`
|
|
45
|
+
* - `getExportColumns`
|
|
46
|
+
*
|
|
47
|
+
* ####Setting Up the Table
|
|
48
|
+
* #####Table Column Usage
|
|
49
|
+
* The following parameters from table columns are utilized in searching and sorting.
|
|
50
|
+
* searchKey: Key to send to the server side when sorting by this column. If unset, defaults to valuePath. Most commonly used when valuePath uses a relationship property, as searchKey can then just use the relationship name.
|
|
51
|
+
* component: To show the loading spinner on the header during searching, component must be set to a header component that shows a spinner when column.loading is true. fw-column-title is an option that will handle this for you.
|
|
52
|
+
*
|
|
53
|
+
* #####Block invoking
|
|
54
|
+
* This template is typically invoked in block format, wrapping around the history search panel. When called in block form, a single hash parameter called actions is provided. This contains the following properties:
|
|
55
|
+
* actions.search: Action to call to use search parameter and fetch entries.
|
|
56
|
+
* actions.export: Action to call to fetch all entries and export them into a CSV file.
|
|
57
|
+
*
|
|
58
|
+
* These actions should be used within the search panel to create search and export buttons.
|
|
59
|
+
* Table
|
|
60
|
+
* The results table can be provided in two forms: block format and parameter format.
|
|
61
|
+
* Parameter format
|
|
62
|
+
* Calling the table in parameter format will use a default fw-table-sortable. In this format, the following additional parameters to fw-pagination-wrapper are available:
|
|
63
|
+
* getTableColumns: Action callback to get the table columns. Callback in case it changes on search, but has no parameters
|
|
64
|
+
* emptyText: Text to display when the table is empty. If unset, hides the table when empty.
|
|
65
|
+
* tableActions: Parameter to send into fw-table-sortable’s tableActions
|
|
66
|
+
* Block format
|
|
67
|
+
* Block format allows passing in a custom table component instead of using fw-table-sortable. In this format, the table is included in the block with the search panel. To distinguish the two, a second parameter, table, is included. It will be an object for the table, and null for the search panel. The code below shows an example of using the block format with both parameters:
|
|
68
|
+
* ```hbs
|
|
69
|
+
* <FwPaginationWrapper
|
|
70
|
+
* …
|
|
71
|
+
* as |actions table|
|
|
72
|
+
* >
|
|
73
|
+
* {{#unless table}}
|
|
74
|
+
* {{!-- Search panel contents --}}
|
|
75
|
+
* {{else}}
|
|
76
|
+
* {{!-- Table component invocation --}}
|
|
77
|
+
* {{/unless}}
|
|
78
|
+
* </FwPaginationWrapper>
|
|
79
|
+
* ```
|
|
80
|
+
* In block format when table is defined, actions contains the following actions:
|
|
81
|
+
* actions.sort: Action to use in fw-table-sortable’s onSort, called when a column is clicked.
|
|
82
|
+
*
|
|
83
|
+
* table contains the following parameters:
|
|
84
|
+
* table.title: Full title for the table, pass to tables as title.
|
|
85
|
+
* table.suffix: Suffix for the table title. Used in some custom table components.
|
|
86
|
+
* table.entries: List of entries to display in the table. Passed to a table as the first unnamed parameter, rows.
|
|
87
|
+
* table.sortKey: Currently active sort key for the table. Passed to a table as defaultSort *
|
|
88
|
+
* ###Server Side
|
|
89
|
+
*
|
|
90
|
+
* The client side performs all history behavior using the browse method of the given model, based on the modelName parameter. The browse route must support several query parameters to handle all cases.
|
|
91
|
+
* Count
|
|
92
|
+
* The count parameter is a boolean that when set to true returns a count of results instead of the results. This is needed to calculate the number of pages available and to show the total in the table title.
|
|
93
|
+
*
|
|
94
|
+
* Count can be performed simply using $this->adapter->count in place of findAll. It takes two parameters, $modelName and $query, same as the first two parameters to findAll. For usage in the table, the result must be placed in a key, “count”, such as with the following code:
|
|
95
|
+
* $count = $this->adapter->count($modelName, $query);
|
|
96
|
+
* return $this->view->helper('json')->add($count, 'count');
|
|
97
|
+
* Limit and Offset
|
|
98
|
+
* The main feature of pagination is the ability to fetch only one page of results at a time, reducing the amount of data fetched in requests.
|
|
99
|
+
* limit:
|
|
100
|
+
* This parameter determines the number of entries per page, as defined by entriesPerPage.
|
|
101
|
+
* This can be accomplished using a filter with $qb->limit.
|
|
102
|
+
* offset:
|
|
103
|
+
* This determines the first entry to be fetched for the limit.
|
|
104
|
+
* This can be accomplished using a filter with $qb->offset.
|
|
105
|
+
*
|
|
106
|
+
* The following code implements both limit and offset:
|
|
107
|
+
* ```js
|
|
108
|
+
* if (isset($options->limit)) {* $limit = $options->limit;* $offset = $options->offset ?? 0;* $this->adapter->addFilter($modelName, function($qb)* use ($limit, $offset) {* $qb->limit($limit);* $qb->offset($offset);* });
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
* Sorting
|
|
112
|
+
* Pagination requires refetching all pages every time the sort order changes, as the first result may not be on the first page. fw-pagination-wrapper handles all the logic needed to do that client side, but it needs to be supported on the server side to work. This requires two parameters:
|
|
113
|
+
* sortKey: Table key to use in sorting
|
|
114
|
+
* ascending: If true, sorts results ascending. If false, sorts them descending.
|
|
115
|
+
*
|
|
116
|
+
* The following code implements sort key and ascending:
|
|
117
|
+
* ```js
|
|
118
|
+
* if (isset($options->sortKey)) {* $sortKey = alias($options->sortKey);* $ascending = ($options->ascending ?? 'false') == 'true';* $ascending = $ascending ? 'ASC' : 'DESC';* $this->adapter->addFilter($modelName, function($qb)* use ($sortKey, $ascending) {* $qb->orderBy($sortKey, $ascending);
|
|
119
|
+
* });
|
|
120
|
+
* }
|
|
121
|
+
* ```
|
|
122
|
+
* In this code, alias is a function that maps the sortKey from the client side name used in the parameter to the server side name supported by SQL. Point of Sales handles this using a function in the model, called using $this->adapter->aliasKey.
|
|
123
|
+
*
|
|
124
|
+
* Note you may need more complex code to sort by complex table fields, such as a relationship (such as Status’s location name) or a computed value (such as Point of Sales’s transaction log total). An example of this can be found in Point of Sales.
|
|
125
|
+
* Design Considerations
|
|
126
|
+
* It is important to note that if you want to use SQL limiting and offsets, you should do all filtering and sorting using SQL. This means that any filters in PHP need to be migrated to SQL or otherwise some pages will return too few results.
|
|
127
|
+
*
|
|
128
|
+
*
|
|
129
|
+
* @class FW-Pagination-Wrapper
|
|
130
|
+
*/
|
|
131
|
+
export default Component.extend({
|
|
132
|
+
layout,
|
|
133
|
+
ajax: inject(),
|
|
134
|
+
config: inject(),
|
|
135
|
+
media: inject(),
|
|
136
|
+
notifications: inject(),
|
|
137
|
+
store: inject(),
|
|
138
|
+
tagName: '',
|
|
139
|
+
|
|
140
|
+
didReceiveAttrs() {
|
|
141
|
+
this._super(...arguments);
|
|
142
|
+
if (this.get('searchOnRender')) {
|
|
143
|
+
this.send('search');
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/*
|
|
148
|
+
* Parameters
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Name of the model in search
|
|
153
|
+
* @type {String}
|
|
154
|
+
* @property modelName
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether or not the table loads on page load. If true, it will call the search based on your defaults already set, whenever the table is rendered for the first time.
|
|
159
|
+
* @type {Boolean}
|
|
160
|
+
* @property searchOnRender
|
|
161
|
+
* @default false
|
|
162
|
+
*/
|
|
163
|
+
searchOnRender: false,
|
|
164
|
+
/**
|
|
165
|
+
* Default sort order for the table
|
|
166
|
+
* @type {String}
|
|
167
|
+
* @property defaultSortKey
|
|
168
|
+
*/
|
|
169
|
+
defaultSortKey: null,
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Number of entries per page
|
|
173
|
+
* @type {Number}
|
|
174
|
+
* @property entriesPerPage
|
|
175
|
+
* @default 100
|
|
176
|
+
*/
|
|
177
|
+
entriesPerPage: 100,
|
|
178
|
+
/**
|
|
179
|
+
* Show the page numbers above the table. If false, they won't show.
|
|
180
|
+
* @type {Boolean}
|
|
181
|
+
* @property showPagesTop
|
|
182
|
+
* @default true
|
|
183
|
+
*/
|
|
184
|
+
showPagesTop: true,
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Show the page numbers below the table. If false, they won't show.
|
|
188
|
+
* @type {Boolean}
|
|
189
|
+
* @property showPagesBottom
|
|
190
|
+
* @default true
|
|
191
|
+
*/
|
|
192
|
+
showPagesBottom: true,
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Gets the title of this table at the given time
|
|
196
|
+
* @property getTitle
|
|
197
|
+
* @type {Action}
|
|
198
|
+
* @return {String} Title of this table
|
|
199
|
+
*/
|
|
200
|
+
getTitle() {
|
|
201
|
+
return 'Table';
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Gets a list of table columns for exporting
|
|
206
|
+
* @property getTableColumns
|
|
207
|
+
* @type {Action}
|
|
208
|
+
* @return {Array} Array of table columns
|
|
209
|
+
*/
|
|
210
|
+
getExportColumns() {},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Action to be called when the table search button is pressed.
|
|
214
|
+
* @type {Function}
|
|
215
|
+
* @property onSearch
|
|
216
|
+
*/
|
|
217
|
+
onSearch() {},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Called to delete the full page of entries
|
|
221
|
+
* Should be passed in with a function
|
|
222
|
+
* @property deletePage
|
|
223
|
+
*/
|
|
224
|
+
deletePage: undefined,
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Determines permission for deleteTablePermission for fw-table-sortable
|
|
228
|
+
* @property deletePagePermission
|
|
229
|
+
* @type {Boolean}
|
|
230
|
+
* @default true
|
|
231
|
+
*/
|
|
232
|
+
deletePagePermission: true,
|
|
233
|
+
/**
|
|
234
|
+
* Makes a query object based on the search fields
|
|
235
|
+
* @property makeQuery
|
|
236
|
+
* @type {Action}
|
|
237
|
+
* @param {Boolean} count If true, counting
|
|
238
|
+
* @param {Number} page If defined, page number for a page search
|
|
239
|
+
* @param {Boolean} export If true, exporting
|
|
240
|
+
* @return {Object} Query object
|
|
241
|
+
*/
|
|
242
|
+
makeQuery(/* {count, page, export} */) {
|
|
243
|
+
return {};
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/* Table generation properties */
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Gets a list of table columns for the results. If null, yields for table
|
|
250
|
+
* @method getTableColumns
|
|
251
|
+
* @return {Array} Array of table columns
|
|
252
|
+
*/
|
|
253
|
+
getTableColumns() {
|
|
254
|
+
return null;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Text to show for empty tables. If null, hides on empty
|
|
259
|
+
* @property emptyText
|
|
260
|
+
* @type {String}
|
|
261
|
+
*/
|
|
262
|
+
emptyText: null,
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Actions to pass into the table
|
|
266
|
+
* @property tableActions
|
|
267
|
+
* @type {Object}
|
|
268
|
+
*/
|
|
269
|
+
tableActions: null,
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Class names for the wrapper around the loading spinner and the table
|
|
273
|
+
* @property tableWrapperClass
|
|
274
|
+
* @type {String}
|
|
275
|
+
*/
|
|
276
|
+
tableWrapperClass: '',
|
|
277
|
+
|
|
278
|
+
/*
|
|
279
|
+
* Search properties
|
|
280
|
+
*/
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Table title at this time
|
|
284
|
+
* @property currentTitle
|
|
285
|
+
* @type {String}
|
|
286
|
+
*/
|
|
287
|
+
currentTitle: null,
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* If true, currently doing the main search
|
|
291
|
+
* @property searchingTable
|
|
292
|
+
* @type {Boolean}
|
|
293
|
+
* @default false
|
|
294
|
+
*/
|
|
295
|
+
searchingTable: false,
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* If true, show export page button
|
|
299
|
+
* @property showExport
|
|
300
|
+
* @type {Boolean}
|
|
301
|
+
* @default true
|
|
302
|
+
*/
|
|
303
|
+
showExport: true,
|
|
304
|
+
/**
|
|
305
|
+
* Number of pages being searched
|
|
306
|
+
* @property pagesSearching
|
|
307
|
+
* @type {Number}
|
|
308
|
+
* @default 0
|
|
309
|
+
*/
|
|
310
|
+
pagesSearching: 0,
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Array of entries, indexes are the pages
|
|
314
|
+
* @property pageEntries
|
|
315
|
+
* @type {Array}
|
|
316
|
+
*/
|
|
317
|
+
pageEntries: null,
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Currently selected page
|
|
321
|
+
* @property page
|
|
322
|
+
* @type {Number}
|
|
323
|
+
* @default 1
|
|
324
|
+
*/
|
|
325
|
+
page: 1,
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Total number of entries
|
|
329
|
+
* @property count
|
|
330
|
+
* @type {Number}
|
|
331
|
+
*/
|
|
332
|
+
count: null,
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Query last time createQuery was called
|
|
336
|
+
* @property lastQuery
|
|
337
|
+
*/
|
|
338
|
+
lastQuery: null,
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Current sort order for the table
|
|
342
|
+
* @type {String}
|
|
343
|
+
* @property currentSortKey
|
|
344
|
+
*/
|
|
345
|
+
currentSortKey: null,
|
|
346
|
+
|
|
347
|
+
/*
|
|
348
|
+
* Computed properties
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Array index for the selected page
|
|
353
|
+
* @type {Number - Computed}
|
|
354
|
+
* @property index
|
|
355
|
+
* @internal
|
|
356
|
+
*/
|
|
357
|
+
index: computed('page', function() {
|
|
358
|
+
return this.get('page') - 1;
|
|
359
|
+
}),
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Array of entries at the current page
|
|
363
|
+
* @type {Array - Computed}
|
|
364
|
+
* @property currentEntries
|
|
365
|
+
* @internal
|
|
366
|
+
*/
|
|
367
|
+
currentEntries: computed('pageEntries.[]', 'index', function() {
|
|
368
|
+
let entries = this.get('pageEntries');
|
|
369
|
+
if (isNone(entries)) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
return entries.objectAt(this.get('index'));
|
|
373
|
+
}),
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Filtered entries, removing deleted entries
|
|
377
|
+
* @type {Array - Computed}
|
|
378
|
+
* @property filteredEntries
|
|
379
|
+
* @internal
|
|
380
|
+
*/
|
|
381
|
+
filteredEntries: filterBy('currentEntries', 'isDeleted', false),
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Current table sort key based on given properties
|
|
385
|
+
* @type {String - Computed}
|
|
386
|
+
* @property tableSortKey
|
|
387
|
+
* @internal
|
|
388
|
+
*/
|
|
389
|
+
tableSortKey: computed('defaultSortKey', 'currentSortKey', function() {
|
|
390
|
+
let current = this.get('currentSortKey');
|
|
391
|
+
if (isEmpty(current)) {
|
|
392
|
+
return `${this.get('defaultSortKey')}:desc`;
|
|
393
|
+
}
|
|
394
|
+
return current;
|
|
395
|
+
}),
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Number of pages available
|
|
399
|
+
* @type {Number - Computed}
|
|
400
|
+
* @property totalPages
|
|
401
|
+
* @internal
|
|
402
|
+
*/
|
|
403
|
+
totalPages: computed('count', 'entriesPerPage', function() {
|
|
404
|
+
let count = this.get('count');
|
|
405
|
+
if (isNone(count)) {
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Math.ceil(count / this.get('entriesPerPage'));
|
|
410
|
+
}),
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* True if we have multiple pages
|
|
414
|
+
* @type {Boolean - Computed}
|
|
415
|
+
* @property showPages
|
|
416
|
+
* @internal
|
|
417
|
+
*/
|
|
418
|
+
showPages: computed('totalPages', function() {
|
|
419
|
+
return this.get('totalPages') > 1;
|
|
420
|
+
}),
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Maximum number of pages to show in the pagination component
|
|
424
|
+
* @type {Number - Computed}
|
|
425
|
+
* @property maxPageButtons
|
|
426
|
+
* @internal
|
|
427
|
+
*/
|
|
428
|
+
maxPageButtons: computed('media.{isMobile,isTablet}', function() {
|
|
429
|
+
let media = this.get('media');
|
|
430
|
+
if (media.get('isMobile')) {
|
|
431
|
+
return 3;
|
|
432
|
+
}
|
|
433
|
+
if (media.get('isTablet')) {
|
|
434
|
+
return 5;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return 7;
|
|
438
|
+
}),
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Gets the serverside route to use for this model name
|
|
442
|
+
* @type {String - Computed}
|
|
443
|
+
* @property routeName
|
|
444
|
+
* @internal
|
|
445
|
+
*/
|
|
446
|
+
routeName: computed('modelName', function() {
|
|
447
|
+
let name = this.get('modelName');
|
|
448
|
+
return this.get('store').adapterFor(name).pathForType(name);
|
|
449
|
+
}),
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Final piece of title for table
|
|
453
|
+
* @type {String - Computed}
|
|
454
|
+
* @property tableSuffix
|
|
455
|
+
* @internal
|
|
456
|
+
*/
|
|
457
|
+
tableSuffix: computed('count', 'currentEntries', 'index', function() {
|
|
458
|
+
let count = this.get('count');
|
|
459
|
+
let entries = this.get('currentEntries');
|
|
460
|
+
if (isEmpty(entries)) {
|
|
461
|
+
return `${count} entries`;
|
|
462
|
+
}
|
|
463
|
+
return `${entries.get('length')} of ${count} entries`;
|
|
464
|
+
}),
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Title for the table
|
|
468
|
+
* @type {String - Computed}
|
|
469
|
+
* @property fullTableTitle
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
fullTableTitle: computed('currentTitle', 'tableSuffix', function() {
|
|
473
|
+
return `${this.get('currentTitle')} - ${this.get('tableSuffix')}`;
|
|
474
|
+
}),
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* If true, hide the table when empty
|
|
478
|
+
* @type {Boolean - Computed}
|
|
479
|
+
* @property hideEmpty
|
|
480
|
+
* @internal
|
|
481
|
+
*/
|
|
482
|
+
hideEmpty: empty('emptyText'),
|
|
483
|
+
|
|
484
|
+
/* Functions */
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Queries the serverside to get the total record count
|
|
488
|
+
* @method queryCount
|
|
489
|
+
* @return {Promise} Promise that resolves to a number
|
|
490
|
+
*/
|
|
491
|
+
queryCount() {
|
|
492
|
+
// fetch standard query
|
|
493
|
+
let query = this.makeQuery({count: true});
|
|
494
|
+
query.count = true;
|
|
495
|
+
|
|
496
|
+
// make request
|
|
497
|
+
let url = this.get('config').formUrl(this.get('routeName'));
|
|
498
|
+
return this.get('ajax').request(url, {data: query}).then((({count}) => count)).catch(handleAjaxError.bind(this));
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Query for settign a new sort order
|
|
503
|
+
* @method querySort
|
|
504
|
+
* @param {Number} page Page number to start
|
|
505
|
+
* @param {String} sortKey New sort order
|
|
506
|
+
* @param {Boolean} ascending If true, sorts ascending, false descending
|
|
507
|
+
* @return {Promise} Promise that resolves to a entry array
|
|
508
|
+
*/
|
|
509
|
+
querySort(page, sortKey, ascending) {
|
|
510
|
+
let query = this.get('lastQuery');
|
|
511
|
+
|
|
512
|
+
// set sort key stuff if present
|
|
513
|
+
if (!isNone(ascending)) {
|
|
514
|
+
query.ascending = ascending;
|
|
515
|
+
}
|
|
516
|
+
if (!isEmpty(sortKey)) {
|
|
517
|
+
query.sortKey = sortKey;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// set limits on query
|
|
521
|
+
let entriesPerPage = this.get('entriesPerPage');
|
|
522
|
+
query.limit = entriesPerPage;
|
|
523
|
+
query.offset = (page - 1) * entriesPerPage;
|
|
524
|
+
|
|
525
|
+
// make promise
|
|
526
|
+
return RSVP.resolve(this.get('store').query(this.get('modelName'), query)).catch(handleAjaxError.bind(this));
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Fetches the entries for the given page number
|
|
531
|
+
* @method queryPage
|
|
532
|
+
* @param {Number} page Page to fetch
|
|
533
|
+
* @return {Promise} Promise that resolves to an entry array
|
|
534
|
+
*/
|
|
535
|
+
queryPage(page) {
|
|
536
|
+
// same as sort, but handles the entries
|
|
537
|
+
return this.querySort(page).then((entries) => {
|
|
538
|
+
this.get('pageEntries')[page - 1] = entries;
|
|
539
|
+
return entries;
|
|
540
|
+
});
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Gets all entries for the given query
|
|
545
|
+
* @method queryAll
|
|
546
|
+
* @return {Promise} Promise that resolves to entries
|
|
547
|
+
*/
|
|
548
|
+
queryAll() {
|
|
549
|
+
let query = this.makeQuery({export: true});
|
|
550
|
+
return RSVP.resolve(this.get('store').query(this.get('modelName'), query)).catch(handleAjaxError.bind(this));
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
actions: {
|
|
554
|
+
/* Search buttons */
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* This action is called when the search button is pressed
|
|
558
|
+
* @method search
|
|
559
|
+
* @return {Promise} Promise that resolves after searching
|
|
560
|
+
*/
|
|
561
|
+
search() {
|
|
562
|
+
// TODO: canSearch?
|
|
563
|
+
// else a title getter
|
|
564
|
+
|
|
565
|
+
// start search and clean up old data
|
|
566
|
+
this.setProperties({
|
|
567
|
+
currentTitle: this.getTitle(),
|
|
568
|
+
// search data
|
|
569
|
+
lastQuery: this.makeQuery({page: 1}),
|
|
570
|
+
pageEntries: [],
|
|
571
|
+
page: 1,
|
|
572
|
+
// searching keys
|
|
573
|
+
searchingTable: true,
|
|
574
|
+
pagesSearching: 0,
|
|
575
|
+
tableColumns: this.getTableColumns()
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// search callback
|
|
579
|
+
this.onSearch();
|
|
580
|
+
|
|
581
|
+
// make two requests: one for the total count and one for the first 100 entries
|
|
582
|
+
return RSVP.hash({
|
|
583
|
+
count: this.queryCount(),
|
|
584
|
+
entries: this.queryPage(1)
|
|
585
|
+
}).then(({count}) => {
|
|
586
|
+
// entries already set as part of queryPage
|
|
587
|
+
this.setProperties({
|
|
588
|
+
count,
|
|
589
|
+
searchingTable: false
|
|
590
|
+
});
|
|
591
|
+
}).catch(() => {
|
|
592
|
+
// request failed, clean up data
|
|
593
|
+
this.setProperties({
|
|
594
|
+
// search data
|
|
595
|
+
pageEntries: null,
|
|
596
|
+
count: 0,
|
|
597
|
+
// searching keys
|
|
598
|
+
searchingTable: false
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* This action is called when the export button is clicked to export all data
|
|
605
|
+
* @method export
|
|
606
|
+
* @return {Promise} Promise that resolves after exporting the table
|
|
607
|
+
*/
|
|
608
|
+
export() {
|
|
609
|
+
// TODO: canSearch?
|
|
610
|
+
|
|
611
|
+
// build table for export
|
|
612
|
+
let table = new Table(this.getExportColumns());
|
|
613
|
+
return this.queryAll().then((entries) => {
|
|
614
|
+
table.setRows(entries.sortBy(this.get('defaultSortKey')).reverse());
|
|
615
|
+
exportTable(table, `${this.getTitle()} - All Entries`);
|
|
616
|
+
}).catch(handleAjaxError.bind(this));
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
/* Pagination */
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* This action is called when a page button is clicked to switch pages
|
|
623
|
+
* @method setPage
|
|
624
|
+
* @param {Number} page New page number to set
|
|
625
|
+
*/
|
|
626
|
+
setPage(page) {
|
|
627
|
+
// clamp page number
|
|
628
|
+
let max = this.get('totalPages');
|
|
629
|
+
if (page < 1) {
|
|
630
|
+
page = 1;
|
|
631
|
+
} else if (page > max) {
|
|
632
|
+
page = max;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// if we havve entries at the page number, use those
|
|
636
|
+
// if missing, query them
|
|
637
|
+
if (isNone(this.get('pageEntries').objectAt(page - 1))) {
|
|
638
|
+
this.incrementProperty('pagesSearching');
|
|
639
|
+
this.set('page', page);
|
|
640
|
+
this.queryPage(page).then(() => {
|
|
641
|
+
// entries set in promise logic
|
|
642
|
+
this.decrementProperty('pagesSearching');
|
|
643
|
+
});
|
|
644
|
+
} else {
|
|
645
|
+
this.set('page', page);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
/* Sorting */
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* This action resorts the entry by the given column
|
|
653
|
+
* @method sortColumn
|
|
654
|
+
* @param {Column} column Column to use for sorting
|
|
655
|
+
* @param {String} sortKey String to use for sorting in the column
|
|
656
|
+
* @return {Promise} Promise that resolves to entries
|
|
657
|
+
*/
|
|
658
|
+
sortColumn(column, sortKey) {
|
|
659
|
+
// if the sort key is unchanged, do nothing
|
|
660
|
+
if (sortKey === this.get('tableSortKey')) {
|
|
661
|
+
return RSVP.resolve();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// mark that we are sorting, column properties do not add desc
|
|
665
|
+
column.set('sorting', true);
|
|
666
|
+
|
|
667
|
+
// search for data
|
|
668
|
+
let page = this.get('page');
|
|
669
|
+
return this.querySort(page, column.searchKey || column.valuePath, column.ascending).then((entries) => {
|
|
670
|
+
// set entries to new list
|
|
671
|
+
let pageEntries = [];
|
|
672
|
+
pageEntries[page - 1] = entries;
|
|
673
|
+
this.setProperties({pageEntries, currentSortKey: sortKey});
|
|
674
|
+
|
|
675
|
+
// mark that we are done sorting
|
|
676
|
+
column.set('sorting', false);
|
|
677
|
+
return entries;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|