arrow-admin
Version:
Arrow Admin Website
1,760 lines (1,528 loc) • 50.8 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();
$('.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;
// save previous step
previousStep = currentStep;
// save current step
currentStep = +data.step;
// remove default class from wizard
$('.steps li span').removeClass('badge-success');
// 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();
var url = objectmodel.config.prefix + '/generate/model/'+encodeURIComponent(modelName);
var success = function(data,status) {
if (data && data.success) {
if (isCurrentModelAPI) {
window.location = 'docs.html?apis/'+modelName+'.html';
}
else {
window.location = 'docs.html?models/'+modelName+'.html';
}
}
else {
//FIXME
}
};
var error = function() {
//FIXME
};
var loader = loadingIndicator($);
$.ajax({
url: url,
data: code,
processData: false,
contentType: 'text/plain; charset=UTF-8',
type: 'POST',
dataType: 'json',
success: success,
error: error
}).always(loader.remove);
}
/************************************************************
* 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 = $(this).val().length>0;
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 (!modelName) {
modelNameValid = false;
modelNameHelp.html(modelNameHelpDefault);
modelNameFormGroup.removeClass('has-error');
}
else {
modelNameValid = true;
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').typeahead('destroy');
$('#child_join_property').val('');
// setup typeahead control for child join model dropdown
$('#child_join_model_container .typeahead').typeahead({
hint: true,
highlight: true,
minLength: 1
},
{
displayKey: 'value',
source: substringMatcher($,joinModelArray)
});
// setup listeners
$('#child_join_model').on('typeahead:selected', function(jq, obj, dataset){
joinModelValid = true;
joinModelChanged=true;
joinModel = $(this).val();
if ($.inArray($(this).val(), usedModelArray) === -1) {
usedModelArray.push($(this).val());
}
joinPropArray = setupPropertyList($,'child_join_property_container', $(this).val(), joinPropArray);
validateStep3($);
});
// child join model key up
$('#child_join_model').keyup(function(e){
if (joinModelArray.indexOf($(this).val()) === -1){
joinModelValid=false;
} else {
joinModelValid=true;
if ($.inArray($(this).val(), usedModelArray) === -1) {
usedModelArray.push($(this).val());
}
joinPropArray = setupPropertyList($,'child_join_property_container', $(this).val(), joinPropArray);
}
joinModel = $(this).val();
joinModelChanged = true;
validateStep3($);
});
// setup listeners
$('#child_join_property').on('typeahead:selected', function(jq, obj, dataset){
joinPropValid = true;
joinPropChanged=true;
joinProp = $(this).val();
validateStep3($);
});
// child join model key up
$('#child_join_property').keyup(function(e){
if (joinPropArray.indexOf($(this).val()) === -1){
joinPropValid=false;
} else {
joinPropValid=true;
}
joinProp = $(this).val();
joinPropChanged = true;
validateStep3($);
});
}
// setup dropdowns with property names for a given model
function setupPropertyList($, elementID, modelName, propertyArray) {
propertyArray = ['id'];
for (var i=0;i<localModels.length;i++) {
if (localModels[i].name === modelName) {
for (var key in localModels[i].fields) {
propertyArray.push(key);
}
break;
}
}
// setup typeahead control for master join property dropdown
$('#'+elementID+ ' .typeahead').typeahead('destroy');
$('#'+elementID+ ' .typeahead').typeahead({
hint: true,
highlight: true,
minLeng