UNPKG

arrow-admin

Version:
669 lines (572 loc) 19.4 kB
/* 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">&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({ 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>'); } }); }); }; });