import { createViewModel } from 'scalejs.metadataFactory';
import { merge, get } from 'scalejs';
import { receive } from 'scalejs.messagebus';
import ko from 'knockout';
import _ from 'lodash';
/**
* Grid component to display a grid of data
*
* @module grid
*
* @param {object} node
* The configuration object for the grid
* @param {object} node.data
* Initial data to populate the grid with
* @param {string} node.id
* The id of the grid
* @param {string} node.classes
* The classes to apply to the grid
* @param {string} node.gridHeaderClasses
* The classes to apply to the grid header
* @param {array} node.gridHeader
* An array of PJSON components to use as the grid header
* @param {object} node.dataSourceEndpoint
* Configuration object for the grid's data source
* @param {object} node.dataSourceEndpoint.target
* Configuration object for the target of the grid's data source
* @param {string} node.dataSourceEndpoint.target.uri
* The uri endpoint for the grid's data source
* @param {object} node.dataSourceEndpoint.target.dataMapFunctions
* An object of functions to run on the data
* @param {string} node.dataSourceEndpoint.target.dataMapFunctions.before
* Function to run before the data is added to the grid?
* @param {string} node.dataSourceEndpoint.target.dataMapFunctions.after
* Function to run after the data is added to the grid?
* @param {object|array} node.dataSourceEndpoint.keyMap
* A mapper object or array of mapper objects to map keys
* @param {object} node.pagination
* An object to specify pagination for the grid
* @param {number} node.pagination.start=0
* The number of which page to start the grid at
* @param {number} node.pagination.limit=15
* The max number of grid items to show on each page
* @param {array} node.columns
* An array of objects to build the columns
* @param {object} node.selection
* A PJSON action to use when a row is selected
* @param {object} node.options
* The options pertaining to the grid
* @param {boolean} node.options.infinite
* Boolean to specify whether to show infinite items on the grid
* @param {boolean} node.options.fixedHeader
* Boolean to specify if the grid header should be fixed or not
* @param {object} node.options.footer
* Configuration object for the grid footer
* @param {boolean} node.options.footer.hideOnDone
* Boolean to hide the footer once the grid is loaded or not
* @param {string} node.options.footer.loadingText
* A string to show while the grid is loading
* @param {string} node.options.footer.doneText
* A string to show when the grid has finished loading.
* @param {object} node.options.hasChildren
* Configuration object for a grid row's child
* @param {string} node.options.hasChildren.showIcon
* The class to apply to the show child button
* @param {string} node.options.hasChildren.hideIcon
* The class to apply to the hide child button
* @param {string} node.options.hasChildren.template
* The template to apply to the child row
* @param {boolean} node.options.hasChildren.onRowSelect
* Boolean to determine whether to show/hide the child on selecting the row or via a button
* @param {boolean} node.options.hasChildren.accordion
* Boolean to determine if only one child should be shown at a time
* @param {boolean} node.options.clientSearch
* Boolean on whether to search/sort client side
* @param {boolean|expression} node.options.gridDisplay
* Boolean or expression to render the grid programmatically
* @param {string} node.options.scrollElement
* Element to scroll grid on, defaults to scrolling on window
*
* @example
* {
* "type": "grid",
* "id": "my_grid_id",
* "classes": "grid-container",
* "gridHeaderClasses": "grid-header",
* "options": {
* "infinite": true,
* "fixedHeader": true,
* "footer": {
* "hideOnDone": true,
* "loadingText": "Loading...",
* "doneText": "Loaded all rows."
* },
* "hasChildren": {
* "showIcon": "icon-open",
* "hideIcon": "icon-close",
* "template": "grid_child_template",
* "onRowSelect": true,
* "accordion": true,
* },
* }
* "dataSourceEndpoint": {
* "target": {
* "uri": "endpoint"
* },
* "keyMap": {
* "resultsKey": "data"
* }
* },
* "pagination": {
* "start": 0,
* "limit": 30
* },
* "columns": [
* {
* "data": "colData",
* "title": "Column Data Title"
* }
* ]
* }
*/
export default function (node) {
const data = node.data,
options = node.options,
columns = node.columns,
context = this,
pagination = node.pagination,
endpoint = node.dataSourceEndpoint,
rows = ko.observableArray(),
skip = ko.observable(pagination.start || 0),
limit = ko.observable(pagination.limit || 15),
search = ko.observable(''),
filters = ko.observable({}),
caseInsensitive = ko.observable(true),
clientSearch = options.clientSearch,
selectedItem = ko.observable({}),
gridContext = {
search,
filters,
rows,
skip,
clientSearch,
caseInsensitive,
getValue: this.getValue,
parentContext: context
},
loaderNoText = '',
loaderLoading = 'Loading more...',
loaderDone = 'Loaded all rows',
loader = {
text: ko.observable(loaderNoText),
done: false,
inProgress: ko.observable(false)
},
subs = [];
let query,
queryCallback,
gridHeaderItems = node.gridHeader || [];
function setupQuery() {
query = createViewModel({
type: 'action',
actionType: 'ajax',
options: endpoint
});
}
function setupGetResponse() {
queryCallback = {
callback: function (err, results) {
if (!err) {
const key = get(endpoint, 'keyMap.resultsKey'),
resultData = key && results ? results[key] : results;
rows.push(...resultData);
skip(results.skip);
loader.inProgress(false);
loader.done = results.skip >= results.total;
if (loader.done) {
loader.text(loaderDone);
} else {
loader.text(loaderNoText);
}
} else {
console.error(`Error in grid query callback: ${err.message || ''}`);
}
}
};
}
function sendQuery(isFilter) {
if (isFilter || (!loader.done && !loader.inProgress())) {
if (isFilter) {
skip(0);
rows.removeAll();
}
query.options.target.data = {
skip: skip(),
limit: limit()
};
// data is in target.data, so it will be sent as is
if (!clientSearch) {
query.options.target.data.search = search();
query.options.target.data.filters = filters();
}
loader.inProgress(true);
loader.text(loaderLoading);
query.action(queryCallback);
// TODO: call a resetfilter function to update skip/row observs in here
}
}
function setupGridHeader() {
gridHeaderItems = gridHeaderItems.map(item => createViewModel.call(gridContext, item));
// TODO: update to createViewModels after Erica updates mf
search.extend({ rateLimit: 1000 });
if (!clientSearch) {
search.subscribe(() => sendQuery(true));
filters.subscribe(() => sendQuery(true));
}
}
function setupRefresh() {
subs.push(receive(`${node.id}.refresh`, () => {
sendQuery(true);
}));
}
function setupSelection() {
if (node.selection) {
selectedItem.subscribe((item) => {
const action = _.cloneDeep(node.selection),
selectionCtx = _.cloneDeep(context);
selectionCtx.data = item;
createViewModel.call(selectionCtx, action).action();
});
}
}
function setupData() {
if (endpoint) {
setupQuery();
setupGetResponse();
sendQuery();
setupRefresh();
} else if (data) {
rows(data);
}
}
function addRow(row) {
rows.push(...row);
}
// Set up a receiver to push rows to the grid.
subs.push(receive(`${node.id}.add`, (row) => {
addRow(row);
}));
setupData();
setupSelection();
setupGridHeader();
return merge(node, {
rows,
columns,
sendQuery,
loader,
options,
gridHeaderItems,
search,
filters,
caseInsensitive,
selectedItem,
dispose: function () {
subs.forEach((sub) => {
sub.dispose();
});
}
});
}