UNPKG

arrow-admin

Version:
1,743 lines (1,515 loc) 53 kB
/* jshint undef: true */ /* global confirm */ /* global ace */ /** * TODO: * ----- * model type combo height on search * fix error handling on save */ var localModelsForListView = []; // only contains model names (no fields) installed locally for all model list views var localModels; var currentStep = 1; var previousStep = 1; var joinStepAdded = false; // add for composite with a join var fieldList = []; // master array for fields added to model var fieldIdCounter = 0; // used for dom ids for fields added var activeFieldRowId = 0; // used to track row selection for model fields (need to save changed value if different row is clicked) var compositeModels = []; // used to track models used in composite models with join var isCurrentModelAPI; var validatorEditor; var getEditor; var setEditor; var reviewEditor; // STEP 1 - STATE VARS var modelName = ''; var modelNameValid = false; var modelNameChanged = false; var modelFieldChanged = false; var modelType = 1; var modelTypeValid = false; var modelTypeChanged = false; var codeModelType = ''; // user to store translation between type selection and code value var modelSource = ''; var modelSourceValid = false; var modelSourceRequired = false; var modelSourceChanged = false; var modelConnector; var modelConnectorRequired = false; var modelConnectorValid = false; var modelConnectorChanged = false; // STEP 2 - STATE VARS var fieldNameValid = false; var dataTypeValid = true; var fieldModelSourceValid = false; var fieldModelSourceRequired = false; var fieldModelSourceChanged = false; var modelFieldValid = false; var modelFieldRequired = false; var modelField = ''; // contains array of fields for a selected model var modelFieldsArray = []; // track when field list changes var fieldListChanged = false; // STEP 3 - STATE VARS var masterModelValid = false; var masterModelChanged = false; var masterModel = ''; var masterModelListArray = []; var masterJoinPropValid = false; var masterJoinPropChanged = false; var masterJoinPropArray = []; var masterJoinProp = ''; var joinTypeValid = false; var joinTypeChanged = false; var joinType = ''; var joinModelValid = false; var joinModelChanged = false; var joinModel = ''; var joinModelListArray = []; var joinPropValid = false; var joinPropChanged = false; var joinPropArray = []; var joinProp = ''; // track models used in joins var usedModelArray = []; // array containing join configuration var joinArray = []; // join obj for code gen var joinObj = {}; // used for ids var joinIDCounter = 0; define(['jquery', 'loader', 'lodash', 'fuelux', 'history', 'datatables', 'datatables-bootstrap', 'bootstrap-editable', 'typeahead'], function ($, Loader, _) { return function () { $('body').addClass('collapsed').addClass('fuelux'); return new Loader('build', function () { $.get('objectmodel', function (objectmodel) { // console.log(objectmodel); main($, objectmodel); }); }); }; }); function main($, objectmodel) { // GENERAL STATE VARS var i; // INITIALIZE CONTROLS var model_connector = $('#model_connector_dd'), connectorHtml = '', memoryFound = false; for (var connectorName in objectmodel.connectors) { if (connectorName !== 'appc.composite') { var connector = objectmodel.connectors[connectorName]; connectorHtml += '<li data-value="' + connectorName + '"><a href="#">' + connectorName + ' (' + connector.description + ')</a></li>\n'; if (connectorName === 'memory') { memoryFound = true; } } } if (!memoryFound) { // add if not configured since it's always available connectorHtml += '<li data-value="memory"><a href="#">memory (In-Memory Database connector)</a></li>\n'; } model_connector.html(connectorHtml); $('#model_connector').combobox(); // save join step html - used for dynamic wizard step var joinStepHtml = $('#join_step').html(); $('#join_step').html(''); // wizard $('#myWizard').wizard(); $('#myWizard').wizard('selectedItem', {step: 1}); // combos $('#model_type').combobox(); $('#join_type').combobox(); // rb groups $('#composite_rb_1').radio(); $('#composite_rb_2').radio(); $('#composite_rb_3').radio(); // modals $('#inherited_fields_modal').modal('hide'); $('#validator_modal').modal('hide'); $('#set_modal').modal('hide'); $('#get_modal').modal('hide'); $('#error_modal').modal('hide'); // tooltips $('.fieldtip').tooltip(); // INITIALIZE CODE EDITORS // validator function editor validatorEditor = ace.edit("validator_editor"); validatorEditor.setTheme("ace/theme/monokai"); validatorEditor.getSession().setMode("ace/mode/javascript"); // 'get' function editor getEditor = ace.edit("get_editor"); getEditor.setTheme("ace/theme/monokai"); getEditor.getSession().setMode("ace/mode/javascript"); // 'set' function editor setEditor = ace.edit("set_editor"); setEditor.setTheme("ace/theme/monokai"); setEditor.getSession().setMode("ace/mode/javascript"); // review final model editor reviewEditor = ace.edit("review_editor"); reviewEditor.setTheme("ace/theme/monokai"); reviewEditor.getSession().setMode("ace/mode/javascript"); reviewEditor.setReadOnly(true); reviewEditor.setHighlightActiveLine(false); // INITIALIZE IN-LINE EDITOR $.fn.editable.defaults.mode = 'popup'; $.fn.editable.defaults.send = 'never'; $.fn.editable.defaults.emptytext = 'Add'; $.fn.editable.defaults.showbuttons = false; $.fn.editable.defaults.highlight = false; localModels = []; for (var modelname in objectmodel.models) { var model = objectmodel.models[modelname]; localModels.push(model); } // // GENERIC FUNCTIONS // // init local model data function init(localModels) { // setup local model list array for (var i = 0; i < localModels.length; i++) { localModelsForListView.push(localModels[i].name); } var matcher = substringMatcher($, localModelsForListView); // setup typeahead fields using models // typeahead field model on page 1 $('#model_source_container .typeahead').typeahead({ hint: true, highlight: true, minLength: 1 }, { displayKey: 'value', source: matcher }); // typeahed for model on page 2 $('#field_model_source_container .typeahead').typeahead({ hint: true, highlight: true, minLength: 1 }, { displayKey: 'value', source: matcher }); } // wizard next button $('#next_btn').click(function (e) { e.preventDefault(); $('.steps li span').removeClass('badge-success'); // remove default class from wizard // handle logic for adding/removing join step dynamically // have to do this in next button - doing it on wizard step change causes infinite loop if (currentStep === 2) { if (modelType === 5) { if (!joinStepAdded) { $('#myWizard').wizard('addSteps', 3, [ { label: 'Join Properties', pane: joinStepHtml } ]); joinStepAdded = true; $('.fieldtip').tooltip(); // HACK AROUND FUELUX WIZARD BUG - when you remove then add steps the step numbering gets jacked - so fix it $('.step-pane').each(function (index) { $(this).attr('data-step', String(index + 1)); }); setupJoinListeners($); } } else { // remove join tab is added but model type is no longer composite with join if (joinStepAdded) { $('#myWizard').wizard('removeSteps', 3); joinStepAdded = false; } } } }); // wizard prev button $('#prev_btn').click(function (e) { e.preventDefault(); $('#next_btn').removeClass('disabled'); $('.steps li span').removeClass('badge-success'); // remove default class from wizard }); // wizard step change - setup state $('#myWizard').on('changed.fu.wizard', function (e, data) { var key, hasFifthStep = modelType === 5; // save previous step previousStep = currentStep; // save current step currentStep = +data.step; // remove default class from wizard $('.steps li span').removeClass('badge-success'); // toggle next / save buttons if ((hasFifthStep && currentStep === 5) || (!hasFifthStep && currentStep === 4)) { $('#next_btn').addClass('hide'); $('.save_model_btn').removeClass('hide'); } else { $('#next_btn').removeClass('hide'); $('.save_model_btn').addClass('hide'); } // HANDLE STEP 1 STATE if (currentStep === 1) { modelTypeChanged = false; modelSourceChanged = false; validateStep1($); } // HANDLE STEP 2 STATE else if (currentStep === 2) { // reset field list state fieldListChanged = false; // if we are composite, the initial view requires a model field if (modelType === 4 || modelType === 5) { modelFieldRequired = true; } // reset data type drop down values $('#data_type > .input-group-btn > ul > li[data-value="1"]').show(); $('#data_type > .input-group-btn > ul > li[data-value="2"]').show(); $('#data_type > .input-group-btn > ul > li[data-value="3"]').show(); $('#data_type').combobox('selectByIndex', '0'); // hide/show correct new field elements if (currentStep > previousStep) { if (modelType === 4 || modelType === 5) { $('#field_name_container').hide(); $('#data_type_container').hide(); $('#composite_select_rb_group').show(); $('#field_model_source_field_container').show(); $('#model_field_select_container').show(); if (modelType === 5) { $('#composite_join_info_message').show(); } } else { $('#composite_select_rb_group').hide(); $('#field_model_source_field_container').hide(); $('#model_field_select_container').hide(); $('#field_name_container').show(); $('#data_type_container').show(); $('#composite_join_info_message').hide(); } } // hide detail if was previously visible and model type or source changed if (modelTypeChanged || modelSourceChanged) { $('#edit_field_detail_form').hide(); // if we are going forward if (currentStep > previousStep) { resetFieldList($); // reset join fields if (modelType === 5 || modelType === 4) { $('#composite_rb_1').radio('check'); $('#field_name_container').hide(); $('#data_type_container').hide(); $('#field_model_source_field_container').show(); $('#model_field_select_container').show(); modelFieldRequired = true; } } } // toggle redcued model field list if (modelType === 2) { $('#add_inherited_fields_containter').show(); } else { $('#add_inherited_fields_containter').hide(); } // set other states based on model type if (modelType === 1 && (modelSourceChanged || modelTypeChanged) && (currentStep > previousStep)) { // find matching model, set inherited fields for (i = 0; i < localModels.length; i++) { if (localModels[i].name === modelSource) { for (key in localModels[i].fields) { var props = {}; props.field = {}; props.field[key] = localModels[i].fields[key]; props.extended = true; addField($, props); } break; } } } // // pre select actions for subsequent step (for some reason they cannot be set before loading - must be a bootstrap bug) $('#action_create_chbx').addClass('checked'); $('#action_read_chbx').addClass('checked'); $('#action_update_chbx').addClass('checked'); $('#action_delete_chbx').addClass('checked'); $('#action_deleteall_chbx').addClass('checked'); // validate step 2 before we move to it validateStep2($); } // if we are on the join page (step 3 and modelType composite with join) // setup dropdowns if (currentStep === 3 && modelType === 5 && (currentStep > previousStep)) { masterModelChanged = false; // reset if (fieldListChanged) { masterModelListArray = []; usedModelArray = []; // populate master dropdown with list of available models for (i = 0; i < fieldList.length; i++) { for (key in fieldList[i].field) { if (fieldList[i].field[key].model) { if ($.inArray(fieldList[i].field[key].model, masterModelListArray) === -1) { masterModelListArray.push(fieldList[i].field[key].model); } } } } // reset $('#master_join_model').val(''); $('#master_join_model_container .typeahead').typeahead('destroy'); // setup typeahead control for model dropdown $('#master_join_model_container .typeahead').typeahead({ hint: true, highlight: true, minLength: 1 }, { displayKey: 'value', source: substringMatcher($, masterModelListArray) }); // reset master properties $('#master_join_property').val(''); // reset join list resetJoinList($); } } // once we are past step three, we have enough data to build our code review model if (currentStep > 3) { // reset editor with current code state of the model reviewEditor.setValue(buildSourceModel()); reviewEditor.clearSelection(); } // save detail values if we moved from step 2 and detail is visible if ($('#edit_field_detail_form').css('display') === 'block') { saveFieldDetailValues($, activeFieldRowId); } }); // build model string to display in review code editor function buildSourceModel() { var key, model = {fields: {}}, replacers = {}, fieldName; // format source model for extended or reduced var sourceModel = (modelSource.length > 0 && (modelType === 1 || modelType === 2)) ? '"' + modelSource + '",' : ''; for (i = 0; i < fieldList.length; i++) { var fieldObject = fieldList[i]; // skip extended fields unless there is a field mapping if (fieldObject.extended) { if (fieldObject.nameChanged) { for (key in fieldObject.field) { model.fields[key] = fieldObject.field[key]; } } else { continue; } // otherwise add field to code } else { for (key in fieldObject.field) { model.fields[key] = fieldObject.field[key]; if (key === model.fields[key].name) { delete model.fields[key].name; } } } if (fieldObject.get) { fieldName = Object.keys(fieldObject.field)[0]; key = '$$' + fieldName + '$get$$'; model.fields[fieldName].get = key; replacers[key] = {name: fieldObject.name, value: fieldObject.get}; } if (fieldObject.set) { fieldName = Object.keys(fieldObject.field)[0]; key = '$$' + fieldName + '$set$$'; model.fields[fieldName].set = key; replacers[key] = {name: fieldObject.name, value: fieldObject.set}; } if (fieldObject.validator) { fieldName = Object.keys(fieldObject.field)[0]; key = '$$' + fieldName + '$validator$$'; model.fields[fieldName].validator = key; replacers[key] = {name: fieldObject.name, value: fieldObject.validator}; } } // set the connector if (modelType === 3) { model.connector = modelConnector; } else if (modelType === 4 || modelType === 5) { model.connector = 'appc.composite'; } // format actions // if any are false, format actions var actions = []; if ($('#action_create_chbx').hasClass('checked')) { actions.push('create'); } if ($('#action_read_chbx').hasClass('checked')) { actions.push('read'); } if ($('#action_update_chbx').hasClass('checked')) { actions.push('update'); } if ($('#action_delete_chbx').hasClass('checked')) { actions.push('delete'); } if ($('#action_deleteall_chbx').hasClass('checked')) { actions.push('deleteAll'); } if (actions.length) { model.actions = actions; isCurrentModelAPI = true; } else { isCurrentModelAPI = false; } if (!$.isEmptyObject(joinObj) && modelType === 5) { model.metadata = joinObj; } model.singular = $('#singular_value').val(); model.plural = $('#plural_value').val(); var replacerString = ''; var body = JSON.stringify(model, null, '\t'); // replace our functions with the real example Object.keys(replacers).forEach(function (key) { var entry = replacers[key]; body = body.replace('"' + key + '"', entry.value.name); replacerString += entry.value.body + '\n\n'; }); // finish! return 'var Arrow = require("arrow");\n\n' + 'var Model = Arrow.' + codeModelType + '(' + sourceModel + '"' + modelName + '",' + body + ");\n\n" + replacerString + '\n' + "module.exports = Model;"; } $('#test_model_btn').click(function () { testModel($, objectmodel); }); $('.save_model_btn').click(function () { saveModel($, objectmodel); }); loadStep1($, objectmodel); loadStep2($, objectmodel); init(localModels); } function testModel($, objectmodel) { var code = reviewEditor.getValue(); } /** * Handle a loading indicator */ function loadingIndicator($) { $('#save_modal').modal('show'); return { remove: function () { $('#save_modal').modal('hide'); } }; } function saveModel($, objectmodel) { var code = reviewEditor.getValue(), url = objectmodel.config.admin.prefix + '/generate/model/' + encodeURIComponent(modelName), loader = loadingIndicator($), pingCount = 0, intervalID; function success(data, status) { if (data && data.success) { // wait for our server to restart before reloading the page var i = setInterval(function () { $.get(objectmodel.config.admin.prefix + '/generate/token/' + data.token, function (obj) { if (obj.success) { loader.remove(); clearInterval(i); if (isCurrentModelAPI) { window.location = 'docs.html?apis/' + modelName + '.html'; } else { window.location = 'docs.html?models/' + modelName + '.html'; } } }).fail(function () { // server is restarting }).error(function () { // server is restarting (old jquery) }); }, 1000); } else { loader.remove(); alert(data.message); } } function error(xhr) { if (xhr.readyState === 0) { intervalID = setInterval(checkServer, 500); } else { loader.remove(); alert('Network Communication Error'); } } function checkServer() { if (++pingCount > 40) { clearInterval(intervalID); loader.remove(); alert('The server did not restart after 10 seconds.\nIt may need more time, or it may have crashed.'); return; } $.ajax({ url: '/arrowPing.json', json: true, success: function (body) { if (body && body.success === true) { clearInterval(intervalID); if (isCurrentModelAPI) { window.location = 'docs.html?apis/' + modelName + '.html'; } else { window.location = 'docs.html?models/' + modelName + '.html'; } } } }); } $.ajax({ url: url, data: code, processData: false, contentType: 'text/plain; charset=UTF-8', type: 'POST', dataType: 'json', success: success, error: error }); } /************************************************************ * Logic for typeahead field - used for model drop downs * ************************************************************/ function substringMatcher($, strs) { return function findMatches(q, cb) { // an array that will be populated with substring matches var matches = []; // regex used to determine if a string contains the substring `q` var substrRegex = new RegExp(q, 'i'); // iterate through the pool of strings and for any string that // contains the substring `q`, add it to the `matches` array $.each(strs, function (i, str) { if (substrRegex.test(str)) { // the typeahead jQuery plugin expects suggestions to a // JavaScript object, refer to typeahead docs for more info matches.push({value: str}); } }); cb(matches); }; } // STEP 1 - FUNCTIONS // toggle next button based on state of step 1 function validateStep1($) { if (modelNameValid && modelTypeValid && ((modelSourceRequired && modelSourceValid) || (!modelSourceRequired)) && ((modelConnectorRequired && modelConnectorValid) || (!modelConnectorRequired))) { $('#next_btn').removeClass('disabled'); } else { $('#next_btn').addClass('disabled'); } } function loadStep1($, objectmodel) { // STEP 1 - EVENT HANDLERS var modelNameHelp = $('#model_name_help'); var modelNameHelpDefault = modelNameHelp.attr('default'); var modelNameFormGroup = modelNameHelp.parent(); // model name change $('#model_name').keyup(function (e) { modelNameValid = /^[a-z][a-z\d\-]*$/i.test($(this).val()); modelName = $(this).val(); if (modelName in objectmodel.models) { modelNameValid = false; modelNameHelp.html('There is already a model with that name. Please use a unique name.'); modelNameFormGroup.addClass('has-error'); } else if (!modelNameValid) { modelNameValid = false; modelNameHelp.html(modelNameHelpDefault); modelNameFormGroup.addClass('has-error'); } else { modelNameHelp.html("Nice name, that's unique!"); modelNameFormGroup.removeClass('has-error'); } modelNameChanged = true; validateStep1($); // default singular/plural values $('#singular_value').val($(this).val()); $('#plural_value').val($(this).val() + 's'); }); // model source selected $('#model_source').on('typeahead:selected', function (jq, obj, dataset) { modelSourceValid = true; modelSource = $(this).val(); modelSourceChanged = true; validateStep1($); }); // model source key changes $('#model_source').keyup(function (e) { if (localModelsForListView.indexOf($(this).val()) === -1) { modelSourceValid = false; } else { modelSourceValid = true; } modelSource = $(this).val(); modelSourceChanged = true; validateStep1($); }); // model type change $('#model_type').on('changed.fu.combobox', function (event, data) { modelType = +data.value; // translate model type for code view at the end codeModelType = (modelType === 1) ? 'Model.extend' : (modelType === 2) ? 'Model.reduce' : 'createModel'; // always hide field detail view on model change $('#edit_field_detail_form').hide(); // hide composite selector $('#composite_select_rb_group').hide(); switch (modelType) { // require source model if extend or reduce case 1: case 2: { $('#model_source_field_container').show(); $('#model_connector_container').hide(); $('#field_model_source_field_container').hide(); modelSourceRequired = true; modelConnectorRequired = false; fieldModelSourceRequired = false; // if not composite then not required break; } default: { // hide if not extend/reduce $('#model_source_field_container').hide(); modelSourceRequired = false; // require connnector value if schema-less if (modelType === 3) { $('#model_connector_container').show(); $('#field_model_source_field_container').hide(); modelConnectorRequired = true; fieldModelSourceRequired = false; // if not composite then not required // required field model name if composite } else { $('#composite_select_rb_group').show(); $('#model_connector_container').hide(); $('#field_model_source_field_container').show(); modelConnectorRequired = false; fieldModelSourceRequired = true; } break; } } modelTypeValid = true; modelTypeChanged = true; validateStep1($); }); // trigger dropdown on click - for some reason this only works when wrapped in a setTimeout $('#model_type').click(function (e) { var w = $(this).css('width'); if (!$('#model_type > .input-group-btn').hasClass('open')) { setTimeout(function (e) { $('#model_type > .input-group-btn').addClass('open'); $('#model_type > .input-group-btn > ul').width(w); }, 1); } }); // model connector change $('#model_connector').on('changed.fu.combobox', function (event, data) { modelConnectorValid = true; modelConnector = data.value; modelConnectorChanged = true; validateStep1($); }); // trigger dropdown on click - for some reason this only works when wrapped in a setTimeout $('#model_connector').click(function (e) { var w = $(this).css('width'); if (!$('#model_connector > .input-group-btn').hasClass('open')) { setTimeout(function (e) { $('#model_connector > .input-group-btn').addClass('open'); $('#model_connector > .input-group-btn > ul').width(w); }, 1); } }); } // STEP 2 - FUNCTIONS // populate field list when source model changes (For composites only) function populateModelFieldList($, modelName) { if (fieldModelSourceChanged && fieldModelSourceValid) { modelFieldsArray = []; for (var i = 0; i < localModels.length; i++) { if (localModels[i].name === modelName) { for (var key in localModels[i].fields) { modelFieldsArray.push(key); } break; } } fieldModelSourceChanged = false; $('#model_field_container .typeahead').typeahead('destroy'); $('#model_field').val(''); // setup typeahead control for master join property dropdown $('#model_field_container .typeahead').typeahead({ hint: true, highlight: true, minLength: 1 }, { displayKey: 'value', source: substringMatcher($, modelFieldsArray) }); } } // // Add a new field to the model. Takes the following properties: // - name // - type // - model (optional - only for composites) // - reduced (optional boolean - only for redcued models) // - extended (optional boolean) - only for extended models) // function addField($, props) { // get field name var fieldName = null; for (var key in props) { if (key === 'field') { for (var fieldKey in props[key]) { fieldName = fieldKey; } } } // see if new field name is already in use if (isDuplicateField($, fieldName)) { return; } // set id var id = fieldIdCounter++; // create html for field row var innerHtml = ''; // for field name, if it's inherited then allow editing if (props.reduced || props.extended || props.compositeInheritedField) { innerHtml += '<span id="edit_field_name_' + id + '" fname="' + fieldName + '" data-title="Change field name">' + fieldName + '</span>'; } else { innerHtml = '<span>' + fieldName + '</span>'; } innerHtml += '<span style="margin-left:10px;color:#999">(' + props.field[fieldName].type + ')</span>'; // we can delete fields unless we are extending a model if (!props.extended) { innerHtml += '<button class="btn btn-xs btn-danger" id="delete_field_btn_' + id + '" rowid="' + id + '">X</button>'; } // composite models require a source model for their field if (props.field[fieldName].model) { innerHtml += '<span style="margin-left:10px;color:#999;float:right;margin-right:10px">' + props.field[fieldName].model + '</label>'; } // reduced fields are inherited - show this if (props.reduced) { innerHtml += '<span style="margin-left:10px;color:#999;float:right;margin-right:10px"><small>inherited<small></label>'; } // extended fields are inherited - show this if (props.extended) { innerHtml += '<span style="margin-left:10px;color:#999;float:right;margin-right:10px"><small>inherited</small></label>'; } // finalize html var inheritedClassName = (props.reduced) ? 'inherited-field' : (props.extended) ? 'inherited-readonly-field' : (props.compositeInheritedField) ? 'composite-inherited-field' : (props.compositeInheritedModel) ? 'composite-inherited-model' : ''; // set row html var outerHtml = '<li id="field_row_' + id + '" row="' + id + '" class="list-group-item ' + inheritedClassName + '">' + innerHtml + '</li>'; // append or set if (fieldList.length === 0) { $('#field_list').html(outerHtml); } else { $('#field_list').append(outerHtml); } // store model fields - save extended because we want to exclude these fields in the model code fieldList.push({id: id, field: props.field, extended: props.extended}); // setup editable field if extended or reduced if (props.extended || props.reduced || props.compositeInheritedField) { $('#edit_field_name_' + id).editable({placement: 'top'}); $('#edit_field_name_' + id).click(function (e) { e.stopPropagation(); }); $('#edit_field_name_' + id).on("shown", function (e, editable) { var $this = $(this); if (arguments.length === 2) { setTimeout(function () { $this.data('editable').input.$input.select(); }, 50); } }); $('#edit_field_name_' + id).on('hidden', function (e, reason) { if (reason === 'save') { var newNameObj = $(this).editable('getValue'); var originalName = $(this).attr('fname'); var newName = null; for (var v in newNameObj) { newName = newNameObj[v]; if (isDuplicateField($, newName)) { $(this).html(originalName); return; } } // find field and update name for (var i = 0; i < fieldList.length; i++) { for (var fv in fieldList[i].field) { if (fv === originalName) { // set name property for mapping fieldList[i].field[fv].name = originalName; fieldList[i].field[newName] = fieldList[i].field[fv]; // delete the old mapping delete fieldList[i].field[fv]; // break out of outter loop i = fieldList.length; break; } } } } }); } // setup delete handler if (!props.extended) { $('#delete_field_btn_' + id).click(function (e) { fieldListChanged = true; // hide detail view $('#edit_field_detail_form').hide(); // delete field from html and field list var id = $(this).attr('rowid'); $('#field_row_' + id).remove(); for (var i = 0; i < fieldList.length; i++) { if (fieldList[i].id === +id) { fieldList.splice(i, 1); // if field list is empty if (fieldList.length === 0) { resetFieldList($); } validateStep2($); break; } } }); } // setup click actions for field to show/hide detail view of field properties $('#field_row_' + id).click(function (e) { var rowId = +$(this).attr('row'); activeFieldRowId = rowId; // see if we need to save state for another row if ($('#field_row_' + activeFieldRowId).hasClass('active')) { saveFieldDetailValues($, activeFieldRowId); } // user has doubled-click on row - remove active state and save values if ($(this).hasClass('active')) { saveFieldDetailValues($, rowId); $('.list-group-item').removeClass('active'); $('#edit_field_detail_form').hide(); } // if it's an extended field - don't show detail view else if ($(this).hasClass('inherited-readonly-field') || $(this).hasClass('composite-inherited-field') || $(this).hasClass('composite-inherited-model')) { $('.list-group-item').removeClass('active'); $('#edit_field_detail_form').hide(); } else { $('.list-group-item').removeClass('active'); $('#edit_field_detail_form').show(); // get detail values for (var i = 0; i < fieldList.length; i++) { // display current values if (fieldList[i].id === rowId) { for (key in fieldList[i].field) { $('#field_description').val(fieldList[i].field[key].description); $('#field_mapping_name').val(fieldList[i].field[key].name); $('#field_default_value').val(fieldList[i].field[key].default); if (fieldList[i].field[key].custom) { $('#field_custom >label').addClass('checked'); } else { $('#field_custom > label').removeClass('checked'); } if (fieldList[i].field[key].readonly) { $('#field_readonly > label').addClass('checked'); } else { $('#field_readonly > label').removeClass('checked'); } if (fieldList[i].field[key].required) { $('#field_required > label').addClass('checked'); } else { $('#field_required > label').removeClass('checked'); } } break; } } $(this).addClass('active'); } }); // check state validateStep2($); } // check for duplicate field names function isDuplicateField($, fieldName) { for (var i = 0; i < fieldList.length; i++) { for (var v in fieldList[i].field) { if (v === fieldName) { $('#error_modal_body').html('Field names must be unique. Please try again.'); $('#error_modal_title').html('Add Field Error'); $('#error_modal').modal('show'); return true; } } } return false; } // reset field list function resetFieldList($) { fieldList = []; fieldListChanged = true; var html = '<ul class="list-group" id="field_list" style="margin-top:5px">'; html += '<li class="list-group-item help-block">Model has no fields</li></ul>'; $('#field_list').html(html); } // return the selected field or null if not selected function getSelectedField() { for (var i = 0; i < fieldList.length; i++) { if (fieldList[i].id === activeFieldRowId) { return fieldList[i]; } } } // save detail values for a field function saveFieldDetailValues($, rowId) { // save any edited values for (var i = 0; i < fieldList.length; i++) { if (fieldList[i].id === rowId) { for (var key in fieldList[i].field) { // logic: if value existed and is now empty - delete property // if value exists write property // description if (fieldList[i].field[key].description && $('#field_description').val().length === 0) { delete fieldList[i].field[key].description; } else if ($('#field_description').val().length > 0) { fieldList[i].field[key].description = $('#field_description').val(); } // default value if (fieldList[i].field[key].default && $('#field_default_value').val().length === 0) { delete fieldList[i].field[key].default; } else if ($('#field_default_value').val().length > 0) { fieldList[i].field[key].default = $('#field_default_value').val(); } // custom if (fieldList[i].field[key].custom && !$('#field_custom > label').hasClass('checked')) { delete fieldList[i].field[key].custom; } else if ($('#field_custom > label').hasClass('checked')) { fieldList[i].field[key].custom = true; } // readonly if (fieldList[i].field[key].readonly && !$('#field_readonly > label').hasClass('checked')) { delete fieldList[i].field[key].readonly; } else if ($('#field_readonly > label').hasClass('checked')) { fieldList[i].field[key].readonly = true; } // required if (fieldList[i].field[key].required && !$('#field_required > label').hasClass('checked')) { delete fieldList[i].field[key].required; } else if ($('#field_required > label').hasClass('checked')) { fieldList[i].field[key].required = true; } } break; } } } // toggle 'add field' and 'next' button based on step 2 state function validateStep2($) { // if we are composite and not a new field (not tied to a model) if ((modelType === 4 || modelType === 5) && !$('#composite_rb_3').radio('isChecked')) { if ($('#composite_rb_1').radio('isChecked')) { if (fieldModelSourceValid && modelFieldValid) { $('#add_field_btn').removeClass('disabled'); } else { $('#add_field_btn').addClass('disabled'); } // else rb 2 is checked } else { if (fieldModelSourceValid && fieldNameValid && dataTypeValid) { $('#add_field_btn').removeClass('disabled'); } else { $('#add_field_btn').addClass('disabled'); } } } else { if (fieldNameValid) { $('#add_field_btn').removeClass('disabled'); } else { $('#add_field_btn').addClass('disabled'); } } // toggle next button // composite joins must have at least two unique models if (modelType === 5) { var models = []; for (var i = 0; i < fieldList.length; i++) { for (var v in fieldList[i].field) { if (fieldList[i].field[v].model) { if ($.inArray(fieldList[i].field[v].model, models) === -1) { models.push(fieldList[i].field[v].model); } } } } if (models.length > 1) { $('#next_btn').removeClass('disabled'); } else { $('#next_btn').addClass('disabled'); } } else { if (fieldList.length > 0) { $('#next_btn').removeClass('disabled'); } else { $('#next_btn').addClass('disabled'); } } } function loadStep2($) { // STEP 2 - EVENT HANDLERS // field name change $('#field_name').keyup(function (e) { if ($(this).val().length > 0) { fieldNameValid = true; } else { fieldNameValid = false; } validateStep2($); }); // field data type change $('#data_type').on('changed.fu.combobox', function (event, data) { dataTypeValid = true; validateStep2($); }); // trigger dropdown on click - for some reason this only works when wrapped in a setTimeout $('#data_type').click(function (e) { var w = $(this).css('width'); if (!$('#data_type > .input-group-btn').hasClass('open')) { setTimeout(function (e) { $('#data_type > .input-group-btn').addClass('open'); $('#data_type > .input-group-btn > ul').width(w); }, 1); } }); // field model source key changes $('#field_model_source').keyup(function (e) { if (localModelsForListView.indexOf($(this).val()) === -1) { fieldModelSourceValid = false; } else { fieldModelSourceValid = true; fieldModelSourceChanged = true; populateModelFieldList($, $(this).val()); } validateStep2($); }); // field model source selected $('#field_model_source').on('typeahead:selected', function (jq, obj, dataset) { fieldModelSourceValid = true; fieldModelSourceChanged = true; populateModelFieldList($, $(this).val()); validateStep2($); }); // model field selected $('#model_field').on('typeahead:selected', function (jq, obj, dataset) { modelFieldValid = true; modelField = $(this).val(); modelFieldChanged = true; validateStep2($); }); // model field key changes $('#model_field').keyup(function (e) { if (modelFieldsArray.indexOf($(this).val()) === -1) { modelFieldValid = false; } else { modelFieldValid = true; } modelField = $(this).val(); modelFieldChanged = true; validateStep2($); }); // composite radio group change $("input[name=composite_rb_group]:radio").change(function () { // reset data type if ($('#composite_rb_1').radio('isChecked')) { $('#field_name_container').hide(); $('#data_type_container').hide(); $('#field_model_source_field_container').show(); $('#model_field_select_container').show(); modelFieldRequired = true; } else if ($('#composite_rb_2').radio('isChecked')) { $('#model_field_select_container').hide(); $('#field_model_source_field_container').show(); $('#field_name_container').show(); $('#data_type_container').show(); $('#data_type > .input-group-btn > ul > li[data-value="1"]').hide(); $('#data_type > .input-group-btn > ul > li[data-value="2"]').hide(); $('#data_type > .input-group-btn > ul > li[data-value="3"]').hide(); $('#data_type').combobox('selectByIndex', '3'); modelFieldRequired = true; } else { $('#field_model_source_field_container').hide(); $('#model_field_select_container').hide(); $('#field_name_container').show(); $('#data_type_container').show(); $('#data_type > .input-group-btn > ul > li[data-value="1"]').show(); $('#data_type > .input-group-btn > ul > li[data-value="2"]').show(); $('#data_type > .input-group-btn > ul > li[data-value="3"]').show(); $('#data_type').combobox('selectByIndex', '0'); modelFieldRequired = false; } }); // add field button $('#add_field_btn').click(function (e) { e.preventDefault(); fieldListChanged = true; var props = {}; props.field = {}; // if we are composite and not a new field (not tied to a model) if ((modelType === 4 || modelType === 5) && !$('#composite_rb_3').radio('isChecked')) { if ($('#composite_rb_1').radio('isChecked')) { var modelName = $('#field_model_source').val(); var datatype = null; // find data type for (var i = 0; i < localModels.length; i++) { if (modelName === localModels[i].name) { for (var v in localModels[i].fields) { if (v === modelField) { datatype = localModels[i].fields[v].type; break; } } } } props.field[modelField] = {model: modelName, type: datatype}; props.compositeInheritedField = true; } else { props.field[$('#field_name').val()] = { model: $('#field_model_source').val(), type: $('#data_type').combobox('selectedItem').text }; props.compositeInheritedModel = true; } } else { props.field[$('#field_name').val()] = {type: $('#data_type').combobox('selectedItem').text}; } // add field addField($, props); // reset fields $('#field_name').val(''); $('#field_model_source').val(''); $('#model_field').val(''); fieldNameValid = false; fieldModelSourceValid = false; modelFieldValid = false; validateStep2($); return false; }); // show modal for fields to select fields from a reduced model $('#select_inherited_fields_btn').click(function (e) { e.preventDefault(); // gather all inherited fields and create select boxes var html = '<div style="float:right;margin-bottom:10px">'; html += '<button class="btn btn-xs btn-default" id="deselect_all_btn">Deselect All</button>'; html += '<button class="btn btn-xs btn-default" style="margin-right:10px" id="select_all_btn">Select All</button>'; html += '</div>'; html += '<table class="table table-striped">'; html += '<thead><tr><th></th><th>Field</th><th>Data Type</th></tr></thead>'; html += '<tbody>'; var count = 0; for (var i = 0; i < localModels.length; i++) { if (localModels[i].name === modelSource) { for (var key in localModels[i].fields) { //console.log('KEY ' + key + ' value type ' + localModels[i].fields[key].type); html += '<tr><td><input type="checkbox" class="inherited_field_chbx" id="inherited_field_' + count + '"/></td><td>' + key + '</td><td>' + localModels[i].fields[key].type + '</td></tr>'; count++; } break; } } html += '</tbody></table>'; // set title and html $('#inherited_fields_modal_label').html('Select Inherited Fields'); $('#inherited_fields_modal_body').html(html); // setup button handlers $('#select_all_btn').click(function (e) { $('.inherited_field_chbx').prop('checked', true); }); $('#deselect_all_btn').click(function (e) { $('.inherited_field_chbx').removeProp('checked'); }); // show modal $('#inherited_fields_modal').modal('show'); }); // button to add fields from a reduced model $('#add_inherited_fields_btn').click(function (e) { $('.inherited-field').remove(); for (var i = 0; i < localModels.length; i++) { if (localModels[i].name === modelSource) { var count = 0; for (var key in localModels[i].fields) { if ($('#inherited_field_' + count).prop('checked')) { var props = {}; props.field = {}; props.field[key] = localModels[i].fields[key]; props.reduced = true; addField($, props); } count++; } break; } } }); // show validator modal editor $('#validator_btn').click(function (e) { e.preventDefault(); validatorEditor.focus(); $('#validator_modal').modal('show'); $('#validator_modal .btn-success').click(function () { var field = getSelectedField(); var src = formatSource(validatorEditor.getValue(), field, 'validate'); field.validator = src; }); }); // show 'get' modal editor $('#get_btn').click(function (e) { e.preventDefault(); getEditor.focus(); $('#get_modal').modal('show'); $('#get_modal .btn-success').click(function () { var field = getSelectedField(); var src = formatSource(getEditor.getValue(), field, 'get'); field.get = src; }); }); // show 'set' modal editor $('#set_btn').click(function (e) { e.preventDefault(); setEditor.focus(); $('#set_modal').modal('show'); $('#set_modal .btn-success').click(function () { var field = getSelectedField(); var src = formatSource(setEditor.getValue(), field, 'set'); field.set = src; }); }); } function formatSource(src, fieldObj, def) { var match = /function\s+(.*)\s*\((.*?)\)\s*{/.exec(src), name = match && match[1], fnargs = match && match[2], body = match && src.substring(match[0].length); // if the default, convert to named function if (name === def) { var n = Object.keys(fieldObj.field)[0]; name += '_' + n; } if (body) { body = 'function ' + name + '(' + fnargs + ') {' + body; } return { name: name, body: body }; } // STEP 3 - FUNCTIONS function setupJoinListeners($) { // master join model selected $('#master_join_model').on('typeahead:selected', function (jq, obj, dataset) { masterModelValid = true; masterModel = $(this).val(); masterModelChanged = true; usedModelArray = []; usedModelArray.push($(this).val()); setupJoinModelList($); validateStep3($); }); // master join model key up $('#master_join_model').keyup(function (e) { if (masterModelListArray.indexOf($(this).val()) === -1) { masterModelValid = false; } else { masterModelValid = true; usedModelArray = []; usedModelArray.push($(this).val()); setupJoinModelList(); } masterModel = $(this).val(); masterModelChanged = true; validateStep3($); }); // master join property selected $('#master_join_property').on('typeahead:selected', function (jq, obj, dataset) { masterJoinPropValid = true; masterJoinProp = $(this).val(); masterJoinPropChanged = true; validateStep3($); }); // master join property key up $('#master_join_property').keyup(function (e) { if (masterJoinPropArray.indexOf($(this).val()) === -1) { masterJoinPropValid = false; } else { masterJoinPropValid = true; } masterJoinProp = $(this).val(); masterJoinPropChanged = true; validateStep3($); }); function joinTypeChangedFn(data) { joinTypeValid = true; joinTypeChanged = true; joinType = (data.value === '1') ? 'inner_join' : 'left_join'; joinObj = {}; joinObj[joinType] = {}; validateStep3($); } // join type change $('#join_type').on('changed.fu.combobox', function (event, data) { joinTypeChangedFn(data); }); // trigger dropdown on click - for some reason this only works when wrapped in a setTimeout $('#join_type').click(function (e) { var w = $(this).css('width'); if (!$('#join_type > .input-group-btn').hasClass('open')) { setTimeout(function (e) { var item = $('#join_type').combobox('selectedItem'); if (!item || !item.text) { $('#join_type').combobox('selectByIndex', '1'); } else { joinTypeChangedFn(item); } $('#join_type > .input-group-btn').addClass('open'); $('#join_type > .input-group-btn > ul').width(w); }, 1); } }); $('#add_join_btn').click(function (e) { // disable parent row $('#master_join_model').attr('disabled', 'disabled'); $('#master_join_property').attr('disabled', 'disabled'); // set html var id = joinIDCounter++; var innerHtml = ''; innerHtml = '<span>' + masterModel + '.' + masterJoinProp + ' = ' + joinModel + '.' + joinProp + '</span>'; innerHtml += '<button class="btn btn-xs btn-danger" id="delete_join_btn_' + id + '" rowid="' + id + '">X</button>'; var outerHtml = '<li id="join_row_' + id + '" row="' + id + '" model="' + joinModel + '" class="list-group-item">' + innerHtml + '</li>'; // append or set if (joinArray.length === 0) { $('#join_list').html(outerHtml); } else { $('#join_list').append(outerHtml); } // setup delete handlers $('#delete_join_btn_' + id).click(function (e) { // delete field from html and field list var id = $(this).attr('rowid'); var model = $('#join_row_' + id).attr('model'); $('#join_row_' + id).remove(); // update join array for (var i = 0; i < joinArray.length; i++) { if (joinArray[i].model === model) { joinArray.splice(i, 1); // if join list is empty if (joinArray.length === 0) { $('#master_join_model').removeAttr('disabled'); $('#master_join_property').removeAttr('disabled'); resetJoinList($); } // update usedModelArray for (var j = 0; j < usedModelArray.length; j++) { if (model === usedModelArray[j]) { usedModelArray.splice(j, 1); setupJoinModelList(); $('#child_join_section').show(); $('#child_no_more_join_section').hide(); break; } } return; } } }); // create code gen var type = null; for (var i = 0; i < fieldList.length; i++) { for (var v in fieldList[i].field) { if (fieldList[i].field[v].model === joinModel) { type = fieldList[i].field[v].type; break; } } } var obj = {}; obj[joinProp] = masterJoinProp; if (/array/i.test(type)) { joinArray.push({model: joinModel, multiple: true, join_properties: obj}); } else { joinArray.push({model: joinModel, join_properties: obj}); } // set join object for code gen joinObj[joinType] = joinArray; // if we've used all of our join models then show message if (usedModelArray.length === masterModelListArray.length) { $('#child_join_section').hide(); $('#child_no_more_join_section').show(); } else { // reset join modelList setupJoinModelList(); } }); } // reset the list of join statements function resetJoinList($) { joinArray = []; joinObj = {}; var html = '<ul class="list-group" id="join_list" style="margin-top:5px">'; html += '<li class="list-group-item help-block">Model has no joins</li></ul>'; $('#join_list').html(html); // show child join fields $('#child_join_section').show(); $('#child_no_more_join_section').hide(); // reset child join model $('#child_join_model_container .typeahead').typeahead('destroy'); $('#child_join_model').val(''); // reset child join property $('#child_join_property_container .typeahead').typeahead('destroy'); $('#child_join_property').val(''); // enable master join fields $('#master_join_model').removeAttr('disabled'); $('#master_join_property').removeAttr('disabled'); } // function to update join model drop down function setupJoinModelList($) { var joinModelArray = []; for (var i = 0; i < masterModelListArray.length; i++) { if ($.inArray(masterModelListArray[i], usedModelArray) === -1) { joinModelArray.push(masterModelListArray[i]); } } // reset child join fields $('#child_join_model_container .typeahead').typeahead('destroy'); $('#child_join_model').val(''); $('#child_join_property_container .typeahead').type