arrow-admin
Version:
Arrow Admin Website
669 lines (572 loc) • 19.4 kB
JavaScript
/* global APIS_URL */
define(['jquery', 'loader', 'toc', 'base64', 'lodash', 'fieldTypes', './appc_table', 'form_params', 'inputmask', 'fuelux', 'datatables', 'datatables-bootstrap', 'datatablesColVis'], 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) {
if (_err.status > 299) {
var message =
'<div class="cms-alert alert alert-warning alert-dismissible fade in" role="alert"> ' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
'<span aria-hidden="true">×</span>' +
'</button>' +
'<strong>Error!</strong><br />' + _message + '<br /><br />' +
'Error Code: ' + _err.status + ' ' + _err.statusText + '<br />' +
'<small>' + _err.responseText + '</small>' +
'</div>';
$('body').prepend(message);
}
console.log(_err);
}
/**
* Helper for making http request
* @param {Object} _options Standard request.js options
*/
function makeRequest(_options) {
var ind = loadingIndicator();
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;
console.log(opts);
$.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) {
console.log('getting model data');
makeRequest({
method: 'GET',
url: APIS_URL + '/' + _model,
success: function (_data) {
_callback(_data);
},
error: function (err) {
showErrorDialog("Couldn't get the necessary data.", err);
}
});
}
/**
* 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 doesntImplementCRUD = item.actions && item.actions.length < 4,
hidden = item.metadata.cms.hide,
apiPresent = (objectModel.apis[prop]) ? true : false;
if (doesntImplementCRUD || 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} _data Object of name value pairs to search for
* @param {Object} _model
*/
function handleSearch(_data, _model) {
if (_data === undefined) {
_data = lastSearch;
}
if (!_data) {
_data = {};
}
lastSearch = _data;
makeRequest({
method: 'GET',
url: APIS_URL + '/' + _model.name + '/query?where=' + encodeURIComponent(JSON.stringify(_data)),
success: function (_data) {
AppcTable.replaceData(table, _data[_data.key]);
}
});
}
/**
* 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 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({}, model);
$(this).attr('disabled', 'disabled');
},
'id': 'cms-clear-filter',
'addTo': 'search'
},
{ // Add refresh button
'text': 'Refresh',
'action': function (el) {
handleSearch(undefined, model);
}
},
{ // Add delete button
'text': 'Delete',
'id': 'cms-delete',
'class': 'btn-danger',
'icon': 'icon-trash',
'hidden': true,
'action': function (el) {
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);
}
});
});
table.row('.selected').remove();
AppcTable.reloadTable(table);
$(this).hide();
}
}
];
if (model.connector !== 'appc.composite') {
buttons.push({
'text': 'New',
'icon': 'icon-plus',
'action': function (e) {
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 {
// TODO disabling objects/arrays for now. How should we handle this?
if (_model.fields[prop].type === 'object' || _model.fields[prop].type === 'array') {
disabled = true;
}
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(data, _model);
// enable clear button
$('#cms-clear-filter').removeAttr('disabled');
});
return {
content: wrapper,
footer: footer
};
}
/**
* Create the detail form
* @param {Object} _model
* @param {Object} _data
*/
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>');
footer.append(deleteBtn);
deleteBtn.on('click', function (e) {
e.preventDefault();
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);
}
});
});
}
footer.on('click', '#submitForm', function () {
wrapper.trigger('submit');
});
wrapper.on('submit', function (e) {
e.preventDefault();
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;
}
}
}
// 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);
}
});
});
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>');
}
});
});
};
});