UNPKG

keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

352 lines (333 loc) 9.54 kB
/** * Helper method to handle List operations, e.g. creating items, deleting items, * getting information about those lists, etc. */ const listToArray = require('list-to-array'); const qs = require('qs'); const xhr = require('xhr'); const assign = require('object-assign'); // Filters for truthy elements in an array const truthy = (i) => i; /** * Get the columns of a list, structured by fields and headings * * @param {Object} list The list we want the columns of * * @return {Array} The columns */ function getColumns (list) { return list.uiElements.map((col) => { if (col.type === 'heading') { return { type: 'heading', content: col.content }; } else { var field = list.fields[col.field]; return field ? { type: 'field', field: field, title: field.label, path: field.path } : null; } }).filter(truthy); } /** * Make an array of filters an object keyed by the filtering path * * @param {Array} filterArray The array of filters * * @return {Object} The corrected filters, keyed by path */ function getFilters (filterArray) { var filters = {}; filterArray.forEach((filter) => { filters[filter.field.path] = filter.value; }); return filters; }; /** * Get the sorting string for the URI * * @param {Array} sort.paths The paths we want to sort * * @return {String} All the sorting queries we want as a string */ function getSortString (sort) { return sort.paths.map(i => { // If we want to sort inverted, we prefix a "-" before the sort path return i.invert ? '-' + i.path : i.path; }).filter(truthy).join(','); }; /** * Build a query string from a bunch of options */ function buildQueryString (options) { const query = {}; if (options.search) query.search = options.search; if (options.filters.length) query.filters = JSON.stringify(getFilters(options.filters)); if (options.columns) query.fields = options.columns.map(i => i.path).join(','); if (options.page && options.page.size) query.limit = options.page.size; if (options.page && options.page.index > 1) query.skip = (options.page.index - 1) * options.page.size; if (options.sort) query.sort = getSortString(options.sort); query.expandRelationshipFields = true; return '?' + qs.stringify(query); }; /** * The main list helper class * * @param {Object} options */ const List = function (options) { // TODO these options are possibly unused assign(this, options); this.columns = getColumns(this); this.expandedDefaultColumns = this.expandColumns(this.defaultColumns); this.defaultColumnPaths = this.expandedDefaultColumns.map(i => i.path).join(','); }; /** * Create an item via the API * * @param {FormData} formData The submitted form data * @param {Function} callback Called after the API call */ List.prototype.createItem = function (formData, callback) { xhr({ url: `${Keystone.adminPath}/api/${this.path}/create`, responseType: 'json', method: 'POST', headers: assign({}, Keystone.csrf.header), body: formData, }, (err, resp, data) => { if (err) callback(err); if (resp.statusCode === 200) { callback(null, data); } else { // NOTE: xhr callback will be called with an Error if // there is an error in the browser that prevents // sending the request. A HTTP 500 response is not // going to cause an error to be returned. callback(data, null); } }); }; /** * Update a specific item * * @param {String} id The id of the item we want to update * @param {FormData} formData The submitted form data * @param {Function} callback Called after the API call */ List.prototype.updateItem = function (id, formData, callback) { xhr({ url: `${Keystone.adminPath}/api/${this.path}/${id}`, responseType: 'json', method: 'POST', headers: assign({}, Keystone.csrf.header), body: formData, }, (err, resp, data) => { if (err) return callback(err); if (resp.statusCode === 200) { callback(null, data); } else { callback(data); } }); }; List.prototype.expandColumns = function (input) { let nameIncluded = false; const cols = listToArray(input).map(i => { const split = i.split('|'); let path = split[0]; let width = split[1]; if (path === '__name__') { path = this.namePath; } const field = this.fields[path]; if (!field) { // TODO: Support arbitary document paths if (!this.hidden) { if (path === this.namePath) { console.warn(`List ${this.key} did not specify any default columns or a name field`); } else { console.warn(`List ${this.key} specified an invalid default column: ${path}`); } } return; } if (path === this.namePath) { nameIncluded = true; } return { field: field, label: field.label, path: field.path, type: field.type, width: width, }; }).filter(truthy); if (!nameIncluded) { cols.unshift({ type: 'id', label: 'ID', path: 'id', }); } return cols; }; List.prototype.expandSort = function (input) { const sort = { rawInput: input || this.defaultSort, isDefaultSort: false, }; sort.input = sort.rawInput; if (sort.input === '__default__') { sort.isDefaultSort = true; sort.input = this.sortable ? 'sortOrder' : this.namePath; } sort.paths = listToArray(sort.input).map(path => { let invert = false; if (path.charAt(0) === '-') { invert = true; path = path.substr(1); } else if (path.charAt(0) === '+') { path = path.substr(1); } const field = this.fields[path]; if (!field) { // TODO: Support arbitary document paths console.warn('Invalid Sort specified:', path); return; } return { field: field, type: field.type, label: field.label, path: field.path, invert: invert, }; }).filter(truthy); return sort; }; /** * Load a specific item via the API * * @param {String} itemId The id of the item we want to load * @param {Object} options * @param {Function} callback */ List.prototype.loadItem = function (itemId, options, callback) { if (arguments.length === 2 && typeof options === 'function') { callback = options; options = null; } let url = Keystone.adminPath + '/api/' + this.path + '/' + itemId; const query = qs.stringify(options); if (query.length) url += '?' + query; xhr({ url: url, responseType: 'json', }, (err, resp, data) => { if (err) return callback(err); // Pass the data as result or error, depending on the statusCode if (resp.statusCode === 200) { callback(null, data); } else { callback(data); } }); }; /** * Load all items of a list, optionally passing objects to build a query string * for sorting or searching * * @param {Object} options * @param {Function} callback */ List.prototype.loadItems = function (options, callback) { const url = Keystone.adminPath + '/api/' + this.path + buildQueryString(options); xhr({ url: url, responseType: 'json', }, (err, resp, data) => { if (err) callback(err); // Pass the data as result or error, depending on the statusCode if (resp.statusCode === 200) { callback(null, data); } else { callback(data); } }); }; /** * Constructs a download URL to download a list with the current sorting, filtering, * selection and searching options * * @param {Object} options * * @return {String} The download URL */ List.prototype.getDownloadURL = function (options) { const url = Keystone.adminPath + '/api/' + this.path; const parts = []; if (options.format !== 'json') { options.format = 'csv'; } parts.push(options.search ? 'search=' + options.search : ''); parts.push(options.filters.length ? 'filters=' + JSON.stringify(getFilters(options.filters)) : ''); parts.push(options.columns ? 'select=' + options.columns.map(i => i.path).join(',') : ''); parts.push(options.sort ? 'sort=' + getSortString(options.sort) : ''); parts.push('expandRelationshipFields=true'); return url + '/export.' + options.format + '?' + parts.filter(truthy).join('&'); }; /** * Delete a specific item via the API * * @param {String} itemId The id of the item we want to delete * @param {Function} callback */ List.prototype.deleteItem = function (itemId, callback) { this.deleteItems([itemId], callback); }; /** * Delete multiple items at once via the API * * @param {Array} itemIds An array of ids of items we want to delete * @param {Function} callback */ List.prototype.deleteItems = function (itemIds, callback) { const url = Keystone.adminPath + '/api/' + this.path + '/delete'; xhr({ url: url, method: 'POST', headers: assign({}, Keystone.csrf.header), json: { ids: itemIds, }, }, (err, resp, body) => { if (err) return callback(err); // Pass the body as result or error, depending on the statusCode if (resp.statusCode === 200) { callback(null, body); } else { callback(body); } }); }; List.prototype.reorderItems = function (item, oldSortOrder, newSortOrder, pageOptions, callback) { const url = Keystone.adminPath + '/api/' + this.path + '/' + item.id + '/sortOrder/' + oldSortOrder + '/' + newSortOrder + '/' + buildQueryString(pageOptions); xhr({ url: url, method: 'POST', headers: assign({}, Keystone.csrf.header), }, (err, resp, body) => { if (err) return callback(err); try { body = JSON.parse(body); } catch (e) { console.log('Error parsing results json:', e, body); return callback(e); } // Pass the body as result or error, depending on the statusCode if (resp.statusCode === 200) { callback(null, body); } else { callback(body); } }); }; module.exports = List;