Link to home
Start Free TrialLog in
Avatar of Bruce Gust
Bruce GustFlag for United States of America

asked on

How can I update the parent of a parent?

Here's my starting point:

User generated image
When you click on "Loose Cannon Fitness," you're clicking on a link that triggers the "/companies" route. This is part of a more verbose method that's loading up a modal with all of the info...

 
/* ################################################################### */
    /* Global method for opening up the company details view  */
    /* ################################################################### */
    this.showCompanyDetails = (companyId, opts) => {
      const route =
        typeof opts.tempRoute !== 'undefined'
          ? opts.tempRoute.replace(/\//g, '')
          : 'companies';
      const sWidth = window.innerWidth > 550 ? 550 : window.innerWidth - 30;
      const afterOpen = opts.afterOpen || function afterOpen() {};

      $.get(`/${route}/${companyId}`, { ajax: 1 }, resp => {
        // add the content to the modal
        $('#modal-company')
          .find('.slide-panel-content')
          .empty()
          .append(resp);

        // add the content scrollbar
        SimpleScrollbar.initEl(
          $('#modal-company').find('.slide-panel-content')[0]
        );

        // enable tooltips
        $('#modal-company [data-toggle="tooltip"]').tooltip({
          container: $('#modal-company')
        });

        // show the slide panel
        $slider = $('<a href="modal-company" />')
          .bigSlide({
            menu: '#modal-company',
            menuWidth: `${sWidth}px`,
            easyClose: false,
            side: 'right',
            beforeOpen() {
              $('#modal-company').draggable();
            },
            afterOpen
          })
          .trigger('click');

        $('#modal-company').css('display', 'block');
      }).fail(() => {
        self.alert(
          'Error',
          'An unexpected error was encountered while trying to load your company record. Please refresh the page and try again.',
          'error'
        );
      });
    };

Open in new window


And here's the twig where all that info is being funneled to...

<!-- ####################################################################### -->
<!-- This template is meant to be used via AJAX and injected into a modal -->
<!-- ####################################################################### -->

{% include 'partials/includes/flash.html.twig' with {
  flash: msg
} %}

{% if error == false %}
  <header>
    <div class="row top-buttons">
      <div class="col text-left">
        <button type="button" id="btn-close" class="btn btn-light btn-sm">
          <i class="fas fa-fw fa-times"></i>
          Close
        </button>
        {% if data.tags is not empty %}
          {% for t in data.tags %}
            <span class="badge badge-info">{{ t|e }}</span>
          {% endfor %}
        {% endif %}
      </div>
      <div class="col text-right">
        <button type="button"
          id="btn-add"
          class="btn btn-light btn-sm company-add-activity"
          data-prospect-id="{{ data._id|e('html_attr') }}"
          data-route="companies"
          data-company-name="{{ data.name|e('html_attr') }}"
          data-user-id="{{ (data.owner is empty
            ? 'undefined'
            : data.owner._id)|e('html_attr') }}"
          data-toggle="tooltip"
          title="Add Activity">
          <i class="fas fa-fw fa-plus fa-plus-h"></i>
        </button>
        <button type="button"
          id="btn-edit"
          class="btn btn-light btn-sm"
          data-prospect-id="{{ data._id|e('html_attr') }}"
          data-route="companies"
          data-toggle="tooltip"
          title="Edit Company">
          <i class="fas fa-fw fa-pencil fa-pencil-alt"></i>
        </button>
        <button type="button"
          id="btn-delete"
          class="btn btn-light btn-sm"
          data-prospect-id="{{ data._id|e('html_attr') }}"
          data-route="companies"
          data-toggle="tooltip"
          title="Delete Company">
          <i class="fas fa-fw fa-trash"></i>
        </button>
      </div>
    </div>

    <h2>
      {{ data.name|e }}
    </h2>
    <ul class="nav nav-pills nav-pills-slide-out nav-fill"
      id="company-tabs"
      role="tablist">
      <li class="nav-item">
        <a class="nav-link active"
          id="details-tab"
          data-toggle="tab"
          href="#details-panel"
          role="tab"
          aria-controls="details"
          aria-selected="true">
          Details
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link"
          id="history-tab"
          data-toggle="tab"
          href="#history-panel"
          role="tab"
          aria-controls="history"
          aria-selected="false">
          History
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link"
          id="contacts-tab"
          data-toggle="tab"
          href="#contacts-panel"
          role="tab"
          aria-controls="contacts"
          aria-selected="false">
          Contacts
        </a>
      </li>
      <li class="nav-item">
        <a class="nav-link"
          id="files-tab"
          data-toggle="tab"
          href="#files-panel"
          role="tab"
          aria-controls="files"
          aria-selected="false">
          Files
        </a>
      </li>
    </ul>
  </header>

  <div class="body">
    <hr />
    <div class="tab-content" id="myTabContent">
      {% include './partials/company-slide-out/history-tab.html.twig' %}

      {% include './partials/company-slide-out/details-tab.html.twig' %}

      {% include './partials/company-slide-out/contacts-tab.html.twig' %}

      {% include './partials/company-slide-out/files-tab.html.twig' %}
    </div>
  </div>
{% endif %}

Open in new window



The "history-tab.html.twig" piece looks like this:

User generated image
...and the code looks like this:

<!-- ############################################################## -->
<!-- Start the history tab -->
<!-- ############################################################## -->
<div class="tab-pane fade"
  id="history-panel"
  role="tabpanel"
  aria-labelledby="history-tab">
  {% if data.activity is empty %}
    <p class="lead text-center">
      This company does not have any associated activity/history.
    </p>
  {% endif %}

  <div class="container">
    <div class="timeline timeline-global">
      {% for row in data.activity %}
        <div class="timeline-month" title="{{ row._id|e('html_attr') }}">
          {{ row.prettyDate|e }}
          <span>
            {{ row.activities|length }}
            Entries
          </span>
        </div>
        {% for activity in row.activities|sort|reverse %}
          <div class="timeline-section">
            <div class="row">
              <div class="col">
                <div class="timeline-box">
                  <div class="box-title">
                    <i class="fa {{ activity.icon|e('html_attr') }}"
                      aria-hidden="true">

                    </i>
                    <a href="javascript:void(0);"
                      class="user-info"
                      data-user-id="{{ activity.user._id|e('html_attr') }}">
                      <i>
                        {{ activity.user.firstName|e }}
                        {{ activity.user.lastName|e }}
                      </i>
                    </a>
                    <span id="type-{{ activity._id }}">{{ activity.type|e }}</span>
                    <span title="{{ activity.date|date('Y-m-d H:i A') }}"
                      data-toggle="tooltip"
                      data-time="{{ activity.date|e('html_attr') }}">
                      {{ activity.prettyDate|e }}
                    </span>
                  </div>
                  <div class="box-content">
                    <a class="btn btn-sm btn-default float-right"
                      data-toggle="collapse"
                      href="#act-{{ activity._id }}"
                      role="button"
                      aria-expanded="false"
                      aria-controls="act-{{ activity._id }}">
                      Details
                    </a>
                    <div class="collapse" id="act-{{ activity._id }}">
                      {% for key, val in activity.meta %}
                        <div class="box-item">
							{% if key=="date" %}
								<span id="date-{{activity._id}}">{{val|date("M jS, Y \\a\\t g:i a")}} </span>
							{% elseif key=="notes" %}
								<strong>{{key}}</strong>: <span id="notes-{{activity._id}}">{{val}}</span>
							{% else %}
								<strong>{{key}}</strong>: <span>{{val}}</span>
							{% endif %}	
							{% if key=="notes" %}
								<br>
								<a href="#" 
									style="float:right;" 
									class="note-delete" 
									data-activityId="{{activity._id}}" 
									data-toggle="tooltip" 
									title="Delete Activity">
									<i class="fas fa-fw fa-trash"></i>
								</a>
								<a href="#" 
									class="note-link" 
									data-prospect-id="{{ data._id }}"
									data-dashboard="no" 
									data-activity-type="note" 
									data-route="companies" 
									data-company-name="{{ data.name|e('html_attr') }}" 
									data-user-id="{{ (data.owner is empty?'undefined':data.owner._id) }}" 
									data-activityId="{{activity._id}}" 
									data-toggle="tooltip" 
									title="Edit Activity" 
									style="float:right;">
									<i class="fas fa-fw fa-pencil fa-pencil-alt"></i>
								</a>
							{% endif %}
                        </div>
                      {% endfor %}
                    </div>
                    {% if activity.proposal is not empty %}
                      <div class="box-item">
                        <strong>Related to</strong>:
                        <a href="javascript:void(0);"
                          class="open-proposal-details"
                          data-proposal-id="{{ activity.proposal|e(
                            'html_attr'
                          ) }}"
                          data-route="companies">
                          Proposal
                        </a>
                      </div>
                    {% endif %}
                    <div class="clearfix clear-fix"></div>
                  </div>
                </div>
              </div>
            </div>
			{% if activity.type=="call" %}
				<label class="left-date-row text-right" 
					title="{{activity.date|e}}">
					<small>
					<b>Note:</b> {{ activity.date|date("M jS") }} 
					</small>
					<br>
					<small>
					<b>Activity:</b> 
					{{ activity.meta.date|date("M jS")|e }} 
					</small>
				</label>
			{% elseif activity.type=="visit" %}
				<label class="left-date-row text-right" 
					title="{{activity.date|e}}">
					<small>
					<b>Note:</b> {{ activity.date|date("M jS") }} 
					</small>
					<br>
					<small>
					<b>Activity:</b> 
					{{ activity.meta.date|date("M jS")|e }} 
					</small>
				</label>
			{% elseif activity.type=="reminder" %}
				<label class="left-date-row text-right" 
					title="{{activity.date|e}}">
					<small>
					<b>Note:</b> {{ activity.date|date("M jS") }} 
					</small>
					<br>
					<small>
					<b>Activity:</b> 
					{{ activity.meta.date|date("M jS")|e }} 
					</small>
				</label>
			{% else %}
				<!-- this is what's being displayed for anytyhing other than a call, a note or a reminder -->
				<label class="left-date-row text-right" 
					title="{{activity.date}}">
					<small><b>Action:</b> 
					{{ activity.date|date("M jS") }} 
					</small>
				</label>
			{% endif %}
          </div>
        {% endfor %}
      {% endfor %}
    </div>
  </div>
</div>

Open in new window


Look at line #79. You'll notice "note-link." This is giving the user the opportunity to edit the note they're looking at. It's the "pencil" - what the black arrow is pointing to in the above image.

When you click on that, you're activating a pop up window with this code:

 
//you're using this when you want to edit a note
	$(document).on('click', '.note-link', function () {
		var activityId=$(this).attr('data-activityId');
		var dashboard = $(this).attr('data-dashboard');
		var id = (typeof $(this).attr('data-company-id') != 'undefined') ? $(this).attr('data-company-id') : $(this).attr('data-prospect-id');
		var cId = id, 
		pId = $(this).attr('data-proposal-id'),
		type = $(this).attr('data-activity-type');
		activityId = $(this).attr('data-activityId');
		route = ((typeof $(this).attr('data-route') == 'undefined') ? route : $(this).attr('data-route'));

		if (typeof cId == 'undefined' || typeof type == 'undefined') {
			CAMS.alert('Invalid Link', 'The selected link is not properly formatted. Please refresh the page or contact support.', 'error');
		}

		CAMS.displayActivityForm(cId, activityId, dashboard); //this is what's creating your fields
	});

Open in new window


...and the "displayActivityForm" method looks like this:

/* ################################################################### */
/* displayActivityForm with values in fields so you can edit the entry  */
/* ################################################################### */
	
 this.displayActivityForm = function (companyId, activityId, dashboard) {
	$.get('/companies/activities/' +activityId, function(resp) {
		if (resp.error) {
			CAMS.alert('Error', resp.msg, 'error');
			return false;
		}
	let type = resp.type;
	let notes = resp.meta.notes;
	let date = moment(resp.meta.date).format("MM/DD/YYYY h:mm:s A");

	var model = objectModels[type],
		fc = model.singular.substring(0, 1),
		config = {
			title: 'Edit a' + ((fc == 'a' || fc == 'e' || fc == 'i' || fc == 'o' || fc == 'u') ? 'n' : '') + ' ' + ucwords(model.singular),
			directionsText: 'Make your changes below and then click, "SAVE."',
			fields: model.fields,
			onShow: function (self) {
				// add in the additional events and plugins
				// add in datetime support
				self.$el.find('input.field-datetime').datepicker({
					autoClose: true,
					language: 'en',
					timepicker: true,
					dateFormat: 'mm/dd/yyyy',
					timeFormat: 'h:ii AA',
					showEvent: 'focus',
					minutesStep: 5
				});
				self.$el.find('#formit-notes').val(notes);
				self.$el.find('#formit-date').val(date);
				self.$el.find('select.form-control.displayType').val(type);

			},
			ajax: {
				path: '/companies/update-activity',
				params: {
					company: companyId,
					activityId: activityId
				},
				onComplete: function (resp, self) {
				if (resp.error) {
					self.addMsg(resp.msg);
					return false;
				}
					if(dashboard=="yes") { //slightly different "success" message for the dashboard
						CAMS.alert('You have successfully updated your activity! Please refresh your page to see the changes.');
						return false;
					}
					else {
						$('#notes-'+activityId).text(resp.notes);
						$('#date-'+activityId).text(moment(resp.date).format("MMM Do, YYYY h:mm A"));
						$('#type-'+activityId).text(resp.type);
						CAMS.alert('You have successfully updated your activity!');
						return false
					}
				}
			}
		};
	// show the form
	formIt.show(config);
	});
};

Open in new window


It's using "formit.js" to create a form and it looks like this:

User generated image
and just for grins, here's the formit.js code:

'use strict';

function FormIt(options) {
  // Create the defaults we will extend
  this.defaults = {
    fields: {},
    title: 'New Form',
    directionsText: 'Complete the form below.',
    directionsClass: 'lead',
    containerClass: 'mfp-hide white-popup white-popup-md',
    submitButtonText: '<i class="fas fa-fw fa-check"></i> Save',
    submitButtonClass: 'btn btn-success',
    cancelButtonText: 'Cancel',
    cancelButtonClass: 'btn btn-link',
    loadDataFromUrl: '',
    onShow: function(self){},
    onDataReceived: function(data, self){ return data; },
    onDataLoaded: function(data, self){},
    onValid: function(data, self){},
    onSubmit: function(data, self){}
  };

  // create a new form uniq id
  this.formId = this._uniqid();

  // establish the opts that will be used
  this.opts = {};
  this.$el;
}

FormIt.prototype.init = function() {
  // create the base container to use
  var html = '<div id="formit-' + this.formId + '" class="' + this.opts.containerClass + '">' +
      '<h3>' + this.opts.title + '</h3>' +
      '<hr />' +
      '<p class="' + this.opts.directionsClass + '">' + this.opts.directionsText + '</p>' +
      '<div class="formit-errors"></div>' +
      '<div class="loading">' +
        '<h3 class="text-center"><i class="fas fa-fw fa-cog fa-spin"></i> Loading...</h3>' +
      '</div>' +
      '<form class="needs-validation d-none" novalidate>' +
        '<div class="field-list"></div>' +
        '<hr />' +
        '<div class="buttons-footer">' +
          '<button type="submit" class="btn-formit-submit ' + this.opts.submitButtonClass + '">' + this.opts.submitButtonText + '</button>' +
          '<button type="button" class="btn-formit-cancel ' + this.opts.cancelButtonClass + '">' + this.opts.cancelButtonText + '</button>' +
        '</div>' +
      '</form>' +
    '</div>';

  if ($('#formit-' + this.formId).length === 0) {
    $('body').append(html);
  } else {
    $('#formit-' + this.formId).replaceWith(html);
  }

  this.$el = $('#formit-' + this.formId);

  // add in the events
  this._addEvents();
};

FormIt.prototype.show = function(opts) {
  // create the instance specific options
  this.opts = $.extend({}, this.defaults, opts);

  // ensure there are fields for the form
  if ( this._getObjectKeys(this.opts.fields).length === 0 ) {
    throw 'Invalid fields option provided.';
  }

  // delete any existing form
  $('div[id^="formit-"]').remove();

  // create the base form
  this.init();

  // build the html form
  this._buildForm();

  // open the form
  $.magnificPopup.open({
    alignTop: true,
    items: {
      type: 'inline',
      src: this.$el
    }
  });

  // hide the loading screen if we do not need to load any external data
  if (this.opts.loadDataFromUrl === '') {
    this.hideLoading();

    this.opts.onDataLoaded({}, this);
  } else {
    this.loadData();
  }

  // call the callback
  if (typeof this.opts.onShow == 'function') {
    this.opts.onShow(this);
  }
};

FormIt.prototype.close = function() {
  $.magnificPopup.close();
};

FormIt.prototype.getData = function() {
  var data = {},
      fields = this.$el.find('form').serializeArray();

  for (var i in fields) {
    data[ fields[i].name ] = fields[i].value;
  }

  return data;
};

FormIt.prototype.loadData = function() {
  // set a local reference to the object for inside the ajax callback
  var self = this;

  // show the loading screen
  this.showLoading();

  // look up the data
  $.get(this.opts.loadDataFromUrl, {}, function(resp){
    if (resp.error) {
      self.close();
      return CAMS.alert('Oh no!', resp.msg, 'error');
    }

    // set a reference
    var data = (typeof resp.data.rows != 'undefined') ? resp.data.rows[0] : resp.data;

    // call a lifecycle method
    data = self.opts.onDataReceived(data, this);

    // stop on false
    if (!data) {
      return self.close();
    }

    // load the form
    self._loadDataIntoForm(data);

    // show the form
    self.hideLoading();
  }, 'json').fail(function(){
    self.close();
    CAMS.alert('Oh no!', 'An unexpected error was encountered while trying to load your data. Please try again.', 'error');
  });
};

FormIt.prototype.reset = function(options) {

};

FormIt.prototype.addMsg = function(msg, type) {
  type = type || 'danger';

  this.clearMsg();

  this.$el.find('.formit-errors').append('<p class="alert alert-'+type+'">'+msg+'</p>');
};

FormIt.prototype.clearMsg = function() {
  this.$el.find('.formit-errors').empty();
};

FormIt.prototype.hideLoading = function() {
  this.$el.find('.loading').addClass('d-none');
  this.$el.find('form').removeClass('d-none');
};

FormIt.prototype.showLoading = function() {
  this.$el.find('form').addClass('d-none');
  this.$el.find('.loading').removeClass('d-none');
};

FormIt.prototype._addEvents = function() {
  var self = this;

  // add in the submission form
  this.$el.on('submit', 'form', function(e){
    var $f = $(this);

    e.preventDefault();
    e.stopPropagation();

    $f.addClass('was-validated');

    if ($f[0].checkValidity() === false) {
      self.addMsg('Please check the highlighted fields.');
      return false;
    }

    self.clearMsg();

    var data = self.getData(),
        resp;

    if (typeof self.opts.onValid == 'function') {
      resp = self.opts.onValid(data, self);

      if (resp === false) {
        return false;
      }
    }

    if (typeof self.opts.onSubmit == 'function') {
      resp = self.opts.onSubmit(data, self);

      if (resp === false) {
        return false;
      }
    }

    if (typeof self.opts.ajax == 'undefined') {
      self.close();
    }

    self._ajax();
  });

  this.$el.on('click', 'button.btn-formit-cancel', function(){
    self.close();
  });

};

FormIt.prototype._ajax = function() {
  if (typeof this.opts.ajax.path == 'undefined' || this.opts.ajax.path === '') {
    throw 'Invalid or missing ajax.path value.';
  }

  var self = this,
      $btn = this.$el.find('button[type="submit"]'),
      btnText = $btn.html(),
      params = (typeof this.opts.ajax.params != 'undefined') ? '&' + this._serialize(this.opts.ajax.params) : '',
      data = this.$el.find('form').serialize() + params;

  $btn.html('<i class="fas fa-fw fa-spinner-third fa-spin"></i> Processing...');

  $.post(this.opts.ajax.path, data, function(resp){
    if (typeof self.opts.ajax.onComplete == 'function') {
      var check = self.opts.ajax.onComplete(resp, self);

      if (check === false) {
        return false;
      }
    }

    self.close();
  }, 'json').fail(function(){
    if (typeof self.opts.ajax.onError == 'function') {
      self.opts.ajax.onError(self);
    }
  }).always(function(){
    $btn.html(btnText)
  });
};

FormIt.prototype._buildForm = function() {
  var fields = [];

  for (var fid in this.opts.fields) {
    fields.push( this._createField(fid, this.opts.fields[fid]) );
  }

  fields.push (this._createField('_csrf', {type: 'hidden', name: '_csrf', value:
      document
      .querySelector('meta[name="csrf-token"]')
      .getAttribute('content')
    }));

  this.$el.find('form .field-list').empty().append( fields.join('') );
};

FormIt.prototype._getObjectKeys = function(obj) {
  var keys = [];

  for (var k in obj) {
    keys.push(k);
  }

  return keys;
}

FormIt.prototype._loadDataIntoForm = function(data) {
  var $field, model;

  for (var fid in data) {
    // load the field
    $field = $('#formit-' + fid);

    if ($field.length === 0) {
      continue;
    }

    // look for the field model
    model = this.opts.fields[fid];

    // load the value based on the type of field
    switch (true) {
      // look for a select2
      case (typeof model.lookup != 'undefined'):
        if (typeof data[fid] == 'object' && typeof data[fid]._id != 'undefined') {
          $field.empty().append('<option value="'+data[fid]._id+'">'+data[fid].text+'</option>');
        }
        break;

      default:
        $field.val( data[fid] );
        break;
    }
  }

  // call the loaded lifecycle method
  this.opts.onDataLoaded(data, this);
};

FormIt.prototype._createField = function(fid, field) {
  var name = field.name || fid,
      label = field.label || this._fieldLabelFromId(fid),
      placeholder = field.placeholder || '',
      classes = field.class || '',
      help = field.help || '',
      input = '',
      value = field.value || '';

  switch (true) {
    // static text
    case (field.type == 'static'):
      var htmlType = field.htmlType || 'p';

      return '<' + htmlType + ' class="' + classes + '">' + label + '</' + htmlType + '>';
      break;

    // check for a select2
    case (typeof field.lookup != 'undefined'):
      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '></select>';
      break;

    // multiline string = textarea
    case (field.type == 'string' && typeof field.multiline != 'undefined' && field.multiline):
      input = '<textarea id="formit-' + fid + '" name="' + name + '" placeholder="' + placeholder + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '></textarea>';
      break;

    // string with contacts is a select2 calling the contacts of the associated company
    case (field.type == 'string' && typeof field.contacts != 'undefined' && field.contacts):
      var opts = ''; // this is a placeholder for when I implement loading data into the form

      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control contacts select2 ' + classes + '"' + ((field.required) ? ' required' : '') + ' data-route="/companies/contacts">' + opts + '</select>';
      break;

    // string with options = select
    case (field.type == 'string' && typeof field.options != 'undefined'):
      var opts = '',
          isArr = Array.isArray(field.options),
          val = '';

      for (var k in field.options) {
        val = (isArr) ? field.options[k] : k;

        opts += '<option value="' + val + '">' + field.options[k] + '</option>';
      }

      input = '<select id="formit-' + fid + '" name="' + name + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + '>' + opts + '</select>';
      break;

      case(field.type == 'hidden'):
      return '<input type="hidden" name="' + name + '" value="' + value + '" />';
      break;

    // string, double, integer, email, datetime
    default:
      let inputType = 'text';
      let attr = ' ';

      if (field.type == 'double' || field.type == 'integer') {
        inputType = 'number';
        attr = (field.type == 'double') ? ' step="0.01"' : ' ';
      } else if (field.type == 'email') {
        inputType = 'email';
      }

      if (field.type == 'datetime') {
        classes += ' field-datetime';
      }

      input = '<input type="' + inputType + '" id="formit-' + fid + '" name="' + name + '" placeholder="' + placeholder + '" class="form-control ' + classes + '"' + ((field.required) ? ' required' : '') + attr + '>';
      break;
  }

  return '<div class="form-group">' +
    '<label for="formit-' + fid + '" class="control-label">' + label + ((field.required) ? '<strong>*</strong>' : '') + '</label>' +
    input +
    ((help !== '') ? '<small class="text-muted"><i class="fas fa-fw fa-question-circle"></i> ' + help + '</small>' : '') +
  '</div>';
};

FormIt.prototype._fieldLabelFromId = function(fid) {
  return (fid + '')
    .replace(/^(.)|\s+(.)/g, function ($1) {
      return $1.toUpperCase()
    });
};

FormIt.prototype._serialize = function(obj, prefix) {
  var str = [],
    p;
  for (p in obj) {
    if (obj.hasOwnProperty(p)) {
      var k = prefix ? prefix + "[" + p + "]" : p,
        v = obj[p];
      str.push((v !== null && typeof v === "object") ?
        serialize(v, k) :
        encodeURIComponent(k) + "=" + encodeURIComponent(v));
    }
  }
  return str.join("&");
};

FormIt.prototype._uniqid = function(prefix, moreEntropy) {
  //  discuss at: http://locutus.io/php/uniqid/
  // original by: Kevin van Zonneveld (http://kvz.io)
  //  revised by: Kankrelune (http://www.webfaktory.info/)
  //      note 1: Uses an internal counter (in locutus global) to avoid collision
  //   example 1: var $id = uniqid()
  //   example 1: var $result = $id.length === 13
  //   returns 1: true
  //   example 2: var $id = uniqid('foo')
  //   example 2: var $result = $id.length === (13 + 'foo'.length)
  //   returns 2: true
  //   example 3: var $id = uniqid('bar', true)
  //   example 3: var $result = $id.length === (23 + 'bar'.length)
  //   returns 3: true

  if (typeof prefix === 'undefined') {
    prefix = ''
  }

  var retId
  var _formatSeed = function (seed, reqWidth) {
    seed = parseInt(seed, 10).toString(16) // to hex str
    if (reqWidth < seed.length) {
      // so long we split
      return seed.slice(seed.length - reqWidth)
    }
    if (reqWidth > seed.length) {
      // so short we pad
      return Array(1 + (reqWidth - seed.length)).join('0') + seed
    }
    return seed
  }

  var $global = (typeof window !== 'undefined' ? window : global)
  $global.$locutus = $global.$locutus || {}
  var $locutus = $global.$locutus
  $locutus.php = $locutus.php || {}

  if (!$locutus.php.uniqidSeed) {
    // init seed with big random int
    $locutus.php.uniqidSeed = Math.floor(Math.random() * 0x75bcd15)
  }
  $locutus.php.uniqidSeed++

  // start with prefix, add current milliseconds hex string
  retId = prefix
  retId += _formatSeed(parseInt(new Date().getTime() / 1000, 10), 8)
  // add seed hex string
  retId += _formatSeed($locutus.php.uniqidSeed, 5)
  if (moreEntropy) {
    // for more entropy we add a float lower to 10
    retId += (Math.random() * 10).toFixed(8).toString()
  }

  return retId
};

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

Open in new window


When I do an update, here's my method:

ajax: {
                        path: '/companies/update-activity',
                        params: {
                              company: companyId,
                              activityId: activityId
                        },
                        onComplete: function (resp, self) {
                        if (resp.error) {
                              self.addMsg(resp.msg);
                              return false;
                        }
                              if(dashboard=="yes") { //slightly different "success" message for the dashboard
                                    CAMS.alert('You have successfully updated your activity! Please refresh your page to see the changes.');
                                    return false;
                              }
                              else {
                                    $('#notes-'+activityId).text(resp.notes);
                                    $('#date-'+activityId).text(moment(resp.date).format("MMM Do, YYYY h:mm A"));
                                    $('#type-'+activityId).text(resp.type);
                                    CAMS.alert('You have successfully updated your activity!');
                                    return false
                              }
                        }
                  }

This is part of the "displayActivityForm" code I showed a moment ago.

How can I complete the update, close the popup and update "history-edit.html.twig?"

It's like the parent of a parent.

What do you think?

Thanks!
history.png
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Hey Bruce,

Not entirely sure you need to access the parent. If you look at the html that's generated in your history twig, it appears that each element has an ID, based on the activity ID:

<span id="date-{{activity._id}}">
<span id="notes-{{activity._id}}">

The activityId is passed in to the edit form when you click the note-link:

this.displayActivityForm = function (companyId, activityId, dashboard) {

Open in new window

The code that fires after an update has been completed is the displayActivityForm.ajax.onComplete method, so you should be able to update you data their:

ajax: {
    path: '/companies/update-activity',
    ...
    onComplete: function (resp, self) {
        ...
        if (resp.error) {
            ...
        }
        if(dashboard=="yes") { //slightly different "success" message for the dashboard
            ...
        } else {
            $('#notes-' + activityId).text(resp.notes)
            ...
            return true;
        }
    }
}

Open in new window

Look at the last couple of lines. The first one builds an ID from the passed in activityId and set's it's text - that should match the element on your page. The last line is changed from return false, to return true. Looking at the formit class, it seems that if you return true, then the edit form should close itself.

You may need to edit your history twig a little to make sure all the elements you want to update have unique IDs (based on the activity ID), but that should allow you to update whatever you want after your AJAX call has completed.
Avatar of Bruce Gust

ASKER

Hey, Chris!

I think you're right in that I've made this far more difficult than it is.

After you do a successful edit, you get this:

User generated image
All I need to do when I click "OK," is close the alert and then close the popup behind it. The updated info is already taken care, so that piece is done. All I need to do is close the alert and the popup behind it.

I'm thinking that this is going to be in the CAMS.js file.

ajax: {
            path: '/companies/update-activity',
            params: {
               company: companyId,
               activityId: activityId
            },
            onComplete: function (resp, self) {
            if (resp.error) {
               self.addMsg(resp.msg);
               return false;
            }
               if(dashboard=="yes") { //slightly different "success" message for the dashboard
                  CAMS.alert('You have successfully updated your activity! Please refresh your page to see the changes.');
                  return false;
               }
               else {
                  $('#notes-'+activityId).text(resp.notes);
                  $('#date-'+activityId).text(moment(resp.date).format("MMM Do, YYYY h:mm A"));
                  $('#type-'+activityId).text(resp.type);
                  CAMS.alert('You have successfully updated your activity!');
                  //window.close();

                  return false;
               }
            }

You can see where I tried a "window.close()," but that didn't work.

What do you think?
ASKER CERTIFIED SOLUTION
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Chris!

I did it! I was able to come up with a solution that replaced some text at the top of the popup rather than triggered an "alert" after the successful update of the record.

But...

My team lead had already made a change, and while I was a little disappointed that my work wasn't going to be used (it deviates from the way the app works across the board - so it's OK), his solution was pretty cool. I've got it below.

this.displayActivityForm = function displayActivityForm(
      companyId,
      activityId,
      isDashboard
    ) {
      $.get(`/companies/activities/${activityId}`, function getActivityById(
        resp
      ) {
        if (resp.error) {
          self.alert('Error', resp.msg, 'error');
        } else {
          const { type } = resp;
          const { notes } = resp.meta;
          const date = moment(resp.meta.date).format('MM/DD/YYYY h:mm:s A');

          const model = objectModels[type];
          const fc = model.singular.substring(0, 1);
          const config = {
            title: `Edit a${
              fc === 'a' || fc === 'e' || fc === 'i' || fc === 'o' || fc === 'u'
                ? 'n'
                : ''
            } ${ucwords(model.singular)}`,
            directionsText: 'Make your changes below and then click, "SAVE."',
            fields: model.fields,
            onShow(e) {
              // add in the additional events and plugins
              // add in datetime support
              e.$el.find('input.field-datetime').datepicker({
                autoClose: true,
                language: 'en',
                timepicker: true,
                dateFormat: 'mm/dd/yyyy',
                timeFormat: 'h:ii AA',
                showEvent: 'focus',
                minutesStep: 5
              });
              e.$el.find('#formit-notes').val(notes);
              e.$el.find('#formit-date').val(date);
              e.$el.find('select.form-control.displayType').val(type);
            },
            ajax: {
              path: '/companies/update-activity',
              params: {
                company: companyId,
                activityId
              },
              onComplete(res) {
                if (res.error) {
                  self.addMsg(resp.msg);
                  return false;
                }

                const companyIdDetails =
                  companyId === 'bulk' ? resp.data.id : companyId;

                $(`#notes-${activityId}`).text(resp.notes);
                $(`#date-${activityId}`).text(moment(resp.date).format("MMM Do, YYYY h:mm A"));
                $(`#type-${activityId}`).text(resp.type);

                // reload the activity stream since this is the dashboard
                if (isDashboard) {
                  $('.dashboard-timeline form').submit();
                } else {
                  $(window).trigger('open-company-details', [
                    companyIdDetails,
                    typeof route !== 'undefined' ? route : '/companies/',
                    'history'
                  ]);
                }
                return true;
              }
            }
          };
          // show the form
          formIt.show(config);
        }
      });
    };

The only part I don't understand is where the "isDashboard" value is coming from.

Cool little dynamic to keep in mind for future projects, though.

Thanks for your input!

BTW: Why does "true" make a difference?
Hey Bruce,

That certainly one way of doing it - basically, after an update, rather than just change the text on screen, it looks like that will just resubmit the form for a full server-side refresh.

As for isDashboard, it's just a parameter to the displayActivityForm function:

this.displayActivityForm = function displayActivityForm(
    companyId,
    activityId,
    isDashboard
)

So when you call displayActivityForm, you're just passing in a boolean as the 3rd argument - displayActivityForm(123, 33, true)

And for the true making a difference, inside your formit class, look for the ajax method. You'll see there that if the call to onComplete returns true, then it self.close() gets fired, so by returning true, the form gets closed.