@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.
Files changed (32) hide show
  1. package/addon/classes/Row.js +74 -74
  2. package/addon/classes/Table.js +147 -147
  3. package/addon/components/fw-cell-action.js +15 -15
  4. package/addon/components/fw-cell-boolean.js +12 -12
  5. package/addon/components/fw-cell-nullable.js +12 -12
  6. package/addon/components/fw-cell-permission-icon.js +12 -12
  7. package/addon/components/fw-column-title.js +13 -13
  8. package/addon/components/fw-delete-modal.js +61 -61
  9. package/addon/components/fw-pagination-wrapper.js +681 -681
  10. package/addon/components/fw-row-toggle-index.js +14 -14
  11. package/addon/components/fw-table-expanded-row.js +24 -24
  12. package/addon/components/fw-table-resort.js +3 -3
  13. package/addon/components/fw-table-sortable.js +398 -389
  14. package/addon/documentation.js +98 -98
  15. package/addon/templates/components/fw-delete-modal.hbs +2 -7
  16. package/addon/templates/components/fw-pagination-wrapper.hbs +45 -74
  17. package/addon/templates/components/fw-table-expanded-row.hbs +23 -23
  18. package/addon/templates/components/fw-table-expanded-rows.hbs +1 -1
  19. package/addon/templates/components/fw-table-resort.hbs +4 -4
  20. package/addon/templates/components/fw-table-sortable.hbs +9 -31
  21. package/addon/utils/base-cells.js +40 -40
  22. package/addon/utils/export.js +76 -76
  23. package/addon/utils/formats.js +46 -46
  24. package/addon/utils/table.js +35 -35
  25. package/app/breakpoints.js +1 -1
  26. package/app/components/fw-cell-permission-icon.js +1 -1
  27. package/app/initializers/responsive.js +1 -1
  28. package/bitbucket-helpers-override.js +240 -240
  29. package/codemods.log +16 -0
  30. package/index.js +9 -10
  31. package/package.json +67 -67
  32. 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
- * {{#fw-pagination-wrapper
70
- * …
71
- * as |actions table|
72
- * }}
73
- * {{#unless table}}
74
- * {{!-- Search panel contents --}}
75
- * {{else}}
76
- * {{!-- Table component invocation --}}
77
- * {{/unless}}
78
- * {{/fw-pagination-wrapper}}
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
+ });