arrow-admin
Version:
Arrow Admin Website
1,743 lines (1,515 loc) • 53 kB
JavaScript
/* 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