arrow-admin
Version:
Arrow Admin Website
723 lines (624 loc) • 21.5 kB
JavaScript
/* global APIS_URL */
define(['jquery', 'loader', 'toc', 'base64', 'lodash', 'fieldTypes', './appc_table', 'form_params', 'inputmask', 'fuelux', 'datatables.net', 'datatables.net-bs', 'datatables.net-buttons'], function ($, Loader, TOC, base64, _, fieldTypes, AppcTable) {
return function () {
return new Loader('cms', true, function () {
var objectModel = null,
lastSearch,
table;
/**
* Error dialog helper
* @param {String} _message
* @param {String} [_err] Error object from error callback
*/
function showErrorDialog(_message, _err) {
var message =
'<div class="cms-alert alert alert-danger alert-dismissible fade in" role="alert"> ' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">×</span>' +
'</button>' +
'<h2>Error!</h2><h4>' + _message + '</h4><br />';
if (_err) {
if (_err.status && _err.status > 299) {
message += 'Error Code: ' + _err.status + ' ' + _err.statusText + '<br />';
}
if (_err.responseText) {
message += '<small>' + _err.responseText + '</small>';
}
if (_err.message) {
message += '<small>' + _err.message + '</small>';
}
}
message += '</div>';
$('body').prepend(message);
}
/**
* Helper for making http request
* @param {Object} _options Standard request.js options
* @param {Function} loading indicator function. Could be a spinning icon, blocking modal, or something custom.
*/
function makeRequest(_options, loader) {
var ind = loader();
var opts = _.defaults(_options, {
accepts: 'text/json'
});
// make sure we're on the same domain so we don't get a cross domain error.
var adminIndex = ADMIN_URL.indexOf('://');
adminIndex = ADMIN_URL.indexOf('/', adminIndex + 3);
var adminURL = document.location.protocol + '//' + document.location.host + ADMIN_URL.substring(adminIndex);
// extract our callback methods from opts.
var success = opts.success,
error = opts.error;
delete opts.success;
delete opts.error;
$.ajax({
url: adminURL,
type: 'post',
data: JSON.stringify(opts),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
if (data && data.body) {
try {
data = JSON.parse(data.body);
}
catch (err) {
// oh well...
}
}
success && success(data);
},
error: error
}).always(function () {
ind.remove();
});
}
/**
* Get specific model's data
* @param {String} _model
* @param {Function} _callback
*/
function getModelData(_model, _callback) {
makeRequest({
method: 'GET',
url: APIS_URL + '/' + _model,
success: function (_data) {
_callback(_data);
},
error: function (err) {
showErrorDialog("Couldn't get the necessary data.", err);
}
}, loadingIndicatorModal);
}
/**
* Add sidebar item to sidebar
* @param {Object} _items
*/
function addSideBarItems(_items) {
var menu = [],
lastMenu;
for (var prop in _items) {
if (!_items.hasOwnProperty(prop)) {
continue;
}
var item = _items[prop];
var hidden = item.metadata.cms.hide,
apiPresent = (objectModel.apis[prop]) ? true : false;
if (hidden || !apiPresent) {
continue;
}
var group = item.metadata.cms.group,
name = getReadableName(item.metadata.cms, prop);
if (item.name.match('/')) {
// Override the group for generated models.
group = item.name.split('/')[0].replace('.', '-');
}
if (group) {
if (!lastMenu || !lastMenu.pages || lastMenu.url !== group) {
menu.push(lastMenu = {title: group, url: group, pages: []});
}
name = name.split('/').pop();
lastMenu.pages.push({title: name, url: prop});
}
else {
menu.push(lastMenu = {title: name, url: prop});
}
}
TOC.renderMenu(false, menu);
}
/**
* Handle manual search of model
* @param {Object} options used for configuring the request url and parameters
* options.operation => the type of operation we execute e.g. refresh, clear, advancedSearch
* options.data => search criteria that must be sent with the request
* options.model => model we are searching for
*/
function handleSearch(options) {
var url = APIS_URL + '/' + options.model.name;
if (options.operation === 'refresh' && lastSearch) {
url = url + '/query?where=' + encodeURIComponent(JSON.stringify(lastSearch));
} else if (options.operation === 'clear') {
lastSearch = undefined;
} else if (options.operation === 'advancedSearch') {
lastSearch = options.data;
url = url + '/query?where=' + encodeURIComponent(JSON.stringify(options.data));
}
makeRequest({
method: 'GET',
url: url,
success: function (_data) {
AppcTable.replaceData(table, _data[_data.key]);
//Hide delete button because all selections are lost after replacing table data
$('#cms-delete').hide();
}
}, loadingIndicator);
}
/**
* Get the readable name
* @param {String} _field
* @param {String} _default
* @returns {String}
*/
function getReadableName(_field, _default) {
return (_field && _field.readableName) ? _field.readableName : _default;
}
/**
* Handle a loading indicator
*/
function loadingIndicator() {
var html = '<div id="cmsLoader"></div>';
$('body').append(html);
return {
remove: function () {
$('#cmsLoader').remove();
}
};
}
/**
* Handle a loading indicator via Modal
*/
function loadingIndicatorModal() {
$('#load_modal').modal('show');
return {
remove: function () {
$('#load_modal').modal('hide');
}
};
}
/**
* Handle navigation
* @param {String} _modelName
*/
function showModelTable(_modelName) {
var model = objectModel.models[_modelName];
getModelData(_modelName, function (_data) {
// Setup columns of table. Always force the checkbox and ID as first
var columns = [{
title: 'id',
data: 'id',
defaultContent: ''
//width: '300px'
}];
for (var prop in model.fields) {
var field = model.fields[prop];
// skip hidden fields on the model
if (field.hidden) { continue; }
// If customization is specified, use that instead
if (model.metadata.cms.defaultColumns.length > 0) {
model.metadata.cms.defaultColumns.forEach(function (_col) {
if (_col === prop) {
columns.push({
title: getReadableName(model.metadata.cms.fields[prop], prop),
data: prop,
defaultContent: ''
//width: '300px'
});
}
});
} else {
columns.push({
title: getReadableName(model.metadata.cms.fields[prop], prop),
data: prop,
defaultContent: ''
//width: '300px'
});
}
}
var buttons = [
{ // Add the search button for sever side search actions
'text': 'Advanced',
'action': function (el) {
openFilterModal(_modelName);
},
'id': 'cms-filter',
'addTo': 'search'
},
{ // Add the filter clear button
'text': 'Clear Filter',
'action': function (el) {
handleSearch({
operation: "clear",
model: model
});
$(this).attr('disabled', 'disabled');
},
'id': 'cms-clear-filter',
'addTo': 'search'
},
{ // Add refresh button
'text': 'Refresh',
'action': function (el) {
handleSearch({
operation: "refresh",
model: model
});
}
}
];
if (model.connector !== 'appc.composite') {
buttons.push({ // Add delete button
'text': 'Delete',
'id': 'cms-delete',
'class': 'btn-danger',
'icon': 'icon-trash',
'hidden': true,
'action': function (el) {
if (model.actions.indexOf('delete') === -1) {
return showErrorDialog('This model does not allow deleting. Please edit its "actions" array to re-enable this feature.');
}
var rows = table.rows('.selected').data();
// TODO Probably should prompt user first before nuking records
rows.each(function (_row) {
makeRequest({
method: 'DELETE',
url: APIS_URL + '/' + _modelName + '/' + _row.id,
error: function (_err) {
showErrorDialog("Couldn't delete the record. Try again.", _err);
}
}, loadingIndicator);
});
table.row('.selected').remove();
AppcTable.reloadTable(table);
$(this).hide();
}
});
buttons.push({
'text': 'New',
'icon': 'icon-plus',
'action': function (e) {
if (model.actions.indexOf('create') === -1) {
return showErrorDialog('This model does not allow creating. Please edit its "actions" array to re-enable this feature.');
}
openDetailModal(model, null);
e.preventDefault();
}
});
}
$('.content').append('<table id="appc_table" class="table table-striped" cellspacing="0" width="90%"></table>');
// TODO figure out how to handle fields with nested objects so they don't look weird in the column
table = AppcTable.createTable({
"data": _data[_data.key],
'title': model.metadata.cms.readableName || _modelName,
"columns": columns,
"createdRow": function (row, data, index) {
$('td', row).wrapInner('<a href="#"></a>');
},
'autoWidth': true,
"stateSave": true,
'buttons': buttons,
'infiniteScrolling': true,
'advancedHeader': true,
'selectRow': function (_event, el) {
if (_event.target.nodeName === 'A') {
var row = table.row(el),
data = row.data();
if (data) {
_event.preventDefault();
openDetailModal(model, data, row);
}
} else if (_event.target.nodeName === 'TD') {
// Check to see how many are selected and show the delete button if appropriate
if (table.rows('.selected').data().length > 0) {
$('#cms-delete').show();
} else {
$('#cms-delete').hide();
}
}
}
}, $('#page-container'));
$('#cms-clear-filter').attr('disabled', 'disabled');
});
}
function showTableButton(id) {
var btn = (typeof id === 'string') ? $(id) : id,
search = $('#appc_table_new_header form.search');
btn.show();
search.css('padding-right', pxToInt(search.css('padding-right')) + btn.outerWidth());
}
function hideTableButton(id) {
var btn = $(id),
search = $('#appc_table_new_header form.search');
search.css('padding-right', pxToInt(search.css('padding-right')) - btn.outerWidth());
btn.hide();
}
function pxToInt(str) {
return parseInt(str.replace('px', ''));
}
/**
* Helper to create modals
* @param {String} _title
* @param {Object} _html
* @param {Object} _html.content HTML/JQuery object for the content
* @param {Object} _html.footer HTML/JQuery object for the footer
* @param {String} _headerRight
* @returns {Object} jquery object
*/
function createModal(_title, _html, _headerRight) {
_headerRight = _headerRight || '<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>';
var modal = $('' +
'<div style="margin-top: 5%;" class="modal-dialog modal-lg">' +
'<div class="modal-content">' +
'<div class="modal-header">' +
_headerRight +
'<h4 class="modal-title">' + _title + '</h4>' +
'</div>' +
'<div class="modal-body">' +
'</div>' +
'<div class="modal-footer">' +
'</div>' +
'</div>' +
'</div>');
modal.find('.modal-body').append(_html.content);
modal.find('.modal-footer').append(_html.footer);
return modal;
}
/**
* Opens the filter modal
* @param {String} _modelName
*/
function openFilterModal(_modelName) {
var model = objectModel.models[_modelName];
var modal = createModal(
'Advanced Search',
createFilterForm(model)
);
$('#filterModal')
.html('')
.append(modal)
.modal('toggle');
}
/**
* Helper to generate form fields from a model schema
* @param {Object} _model
* @param {Object} _data (optional)
* @param {Object} _form JQuery object to append field to
* @param {Boolean} _searchFields Indicate if these fields are search fields for a search form
*/
function createFieldsFromSchema(_form, _model, _data, _searchFields) {
for (var prop in _model.fields) {
var val = '';
if (_data) {
val = (_data[prop] !== undefined) ? _data[prop] : '';
}
var readableName = getReadableName(_model.metadata.cms.fields[prop], prop);
var field = $('<div class="form-group"></div>');
field.append('<label for="' + readableName + '">' + readableName + '</label>');
var renderedField;
var disabled = (_model.metadata.cms.fields[prop] && _model.metadata.cms.fields[prop].disabled) ? true : false;
// TODO need to figure out how to handle composites. CRUD on composites have all sorts of weird
// use cases like...if a required field is necessary in a model but not exposed in the composite.
if (!_searchFields && _model.connector === 'appc.composite') {
disabled = true;
}
// Determine if it's a custom field type or default textfield
if (_model.metadata.cms.fields[prop] && _model.metadata.cms.fields[prop].type) {
renderedField = fieldTypes(_form, _model.metadata.cms.fields[prop], prop, val, disabled);
} else {
renderedField = fieldTypes(_form, _model.fields[prop], prop, val, disabled);
}
// Handle required fields
if (_model.fields[prop].required) {
renderedField.addClass('hasError');
}
field.append(renderedField);
_form.append(field);
}
}
/**
* Create filter form
* @param {Object} _model
*/
function createFilterForm(_model) {
var wrapper = $('<form class="clearfix" role="form" id="detailFormWrapper" method="post" action="#"></form>');
createFieldsFromSchema(wrapper, _model, null, true);
var footer = $(
'<div class="stickyFooter">' +
'<button id="submitForm" type="submit" class="btn pull-right btn-primary">Search</button>' +
'<button type="button" style="margin-right: 5px" id="cancelForm" data-dismiss="modal" class="btn pull-right btn-default">Cancel</button>' +
'</div>'
);
footer.on('click', '#submitForm', function () {
wrapper.trigger('submit');
});
wrapper.on('submit', function (e) {
e.preventDefault();
// don't include empty fields and convert booleans
var data = $(this).formParams(true);
$('#filterModal').modal('toggle').html('');
handleSearch({
operation: "advancedSearch",
data: data,
model: _model
});
// enable clear button
$('#cms-clear-filter').removeAttr('disabled');
});
return {
content: wrapper,
footer: footer
};
}
/**
* Create the detail form
* @param {Object} _model
* @param {Object} _data
* @param _row
*/
function createDetailForm(_model, _data, _row) {
_data = _data || {};
var wrapper = $('<form class="clearfix" role="form" id="detailFormWrapper" method="post" action="#"></form>');
createFieldsFromSchema(wrapper, _model, _data, false);
// TODO need to figure out how to handle composites. CRUD on composites have all sorts of weird
// use cases like...if a required field is necessary in a model but not exposed in the composite.
// For now, just disabling the ability to create a new record based on a composite
var footer = $('<div class="stickyFooter"></div>');
if (_model.connector !== 'appc.composite') {
footer.append('<button id="submitForm" type="submit" class="btn pull-right btn-primary">Save</button>');
}
footer.append('<button type="button" style="margin-right: 5px" id="cancelForm" data-dismiss="modal" class="btn pull-right btn-default">Cancel</button>');
if (_data && _data.id && _model.connector !== 'appc.composite') {
var deleteBtn = $('<a id="deleteRecord" class="btn btn-danger" href="#" role="button">Delete</a>');
deleteBtn.on('click', function (e) {
e.preventDefault();
if (_model.actions.indexOf('delete') === -1) {
return showErrorDialog('This model does not allow deleting. Please edit its "actions" array to re-enable this feature.');
}
makeRequest({
method: 'DELETE',
url: APIS_URL + '/' + _model.name + '/' + _data.id,
success: function (_res) {
$('#detailModal').modal('toggle').html('');
_row.remove();
AppcTable.reloadTable(table);
},
error: function (_err) {
showErrorDialog("Couldn't delete the record. Try again.", _err);
}
}, loadingIndicator);
});
footer.append(deleteBtn);
}
footer.on('click', '#submitForm', function () {
wrapper.trigger('submit');
});
wrapper.on('submit', function (e) {
e.preventDefault();
if (_data.id && _model.actions.indexOf('update') === -1) {
return showErrorDialog('This model does not allow updating. Please edit its "actions" array to re-enable this feature.');
}
var data = $(this).formParams();
var url = APIS_URL + '/' + _model.name;
for (var prop in _model.fields) {
if (_model.fields.hasOwnProperty(prop)) {
var field = _model.fields[prop];
switch (field.type) {
case 'number':
data[field.name || prop] = parseFloat(data[field.name || prop]);
break;
case 'object':
case 'array':
try {
if (data[field.name || prop]) {
data[field.name || prop] = JSON.parse(data[field.name || prop]);
}
}
catch (err) {
showErrorDialog((field.name || prop) + ' is invalid!', err);
return false;
}
break;
}
}
}
// Specially handle for array fields since they get missed in the formParams(); call
var pillboxes = $(this).find('.cms-pillbox');
if (pillboxes) {
pillboxes.each(function () {
var pillKey = $(this).data('key');
data[pillKey] = $(this).pillbox('items').map(function (_row) {
return _row.value;
});
});
}
makeRequest({
method: _data.id ? 'PUT' : 'POST',
url: _data.id ? (url + '/' + _data.id) : url,
body: data,
success: function (_res) {
$('#detailModal').modal('toggle').html('');
getModelData(_model.name, function (_data) {
AppcTable.replaceData(table, _data[_data.key]);
});
},
error: function (_err) {
showErrorDialog("Couldn't " + (_data.id ? 'update' : 'create') + " the record.", _err);
}
}, loadingIndicator);
});
return {
content: wrapper,
footer: footer
};
}
/**
* Open modal detail window
* @param {Object} _model Model schema and such
* @param {Object} _data Model record data
* @param {Object} _row The row of data that was selected (optional)
*/
function openDetailModal(_model, _data, _row) {
_data = _data || {};
var title = _model.metadata.cms.readableName || _model.name;
var modal = createModal(
title,
createDetailForm(_model, _data, _row),
(_data.id) ? '<p class="pull-right text-warning">' + _data.id + '</p>' : null
);
$('#detailModal')
.html('')
.append(modal)
.modal('toggle');
$(document).trigger('cms-form-ready');
}
// Place holders for several initial HTML items
$('body').append('<div class="modal fade" id="detailModal"></div><div class="modal fade" id="filterModal"></div>');
$.get('objectmodel', function (_objModel) {
// Make sure we have CMS objects for our models. This might be temporary until
// we add this object in Arrow
for (var prop in _objModel.models) {
var metadata = _objModel.models[prop].metadata;
metadata.cms = metadata.cms || {};
metadata.cms.fields = metadata.cms.fields || {};
metadata.cms.defaultColumns = metadata.cms.defaultColumns || [];
}
objectModel = _objModel;
// Populate sidebar
addSideBarItems(objectModel.models);
var selected;
function selectModelRow(id) {
showModelTable(id);
if (selected) {
selected.removeClass('selected');
}
var el = $('.cms-list-group-model[data-id="' + id + '"]');
el.addClass('selected');
selected = el;
el.parents('.collapse').addClass('in');
}
// Intro
if (window.location.search) {
var id = window.location.search.substr(1);
if (id.indexOf('/') >= 0) {
id = id.substr(id.indexOf('/') + 1);
}
if (id.indexOf('.html') >= 0) {
id = id.substr(0, id.indexOf('.html'));
}
selectModelRow(id);
}
else {
$('#cmscontainer').html('<div class="jumbotron"><p>Select a model in the menu to get started</p></div>');
}
});
});
};
});