UNPKG

arrow-admin

Version:
630 lines (538 loc) 18.6 kB
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, 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 authstr = 'Basic ' + base64.encode(objectModel.config.apikey + ':'); var ind = loadingIndicator(); var opts = _.defaults(_options, { dataType: 'json', headers: { 'Authorization': authstr, 'Accept': 'application/json' } }); $.ajax(opts).always(function() { ind.remove(); }); } /** * Get specific model's data * @param {String} _model * @param {Function} _callback */ function getModelData(_model, _callback) { makeRequest({ type: 'GET', url: objectModel.config.apiPrefix + '/' + _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) { makeRequest({ type: 'GET', url: objectModel.config.apiPrefix + '/' + _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 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({ url: objectModel.config.apiPrefix + '/' + _modelName + '/' + _row.id, type: 'DELETE', complete: function(_res) { // TODO handle success console.log(_res.responseText); }, 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">&times;</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({ url: objectModel.config.apiPrefix + '/' + _model.name + '/' + _data.id, type: 'DELETE', 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 = objectModel.config.apiPrefix + '/' + _model.name; // 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({ url: (_data.id) ? url + '/' + _data.id : url, type: (_data.id) ? 'PUT' : 'POST', data: JSON.stringify(data), contentType: 'application/json', processData: false, complete: function (_res) { $('#detailModal').modal('toggle').html(''); getModelData(_model.name, function(_data) { AppcTable.replaceData(table, _data[_data.key]); }); }, success: function() { /*// why isn't this called???? getModelData(_model.name, function(_data) { console.log('test'); AppcTable.replaceData(table, _data[_data.key]); });*/ }, error: function (_err) { showErrorDialog("Couldn't 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>'); } }); }); }; });