Ensuring data integrity with master/detail tables

There has been an issue that affects the natural urge of users to “multi-task”. To highlight the issue:

  1. Navigate to a master list page
  2. Open a master/detail Add page for master record 1 …IN A NEW TAB
  3. Switch back to the master list page browser tab
  4. Open another master/detail Add page for master record 2 …IN A NEW TAB
  5. Switch back to the browser tab for master record 1
  6. Add the new detail record
  7. Check the detail records for master record 1 - the new record will not be there
  8. Check the detail records for master record 2 …and the new detail record will appearThe reason for this unwanted behaviour is that PHPM only records details of the most recently viewed master record in the browser session.Previously, the only solutions to this problem were:
  9. never use more than one browser tab (hardly a convenient approach!), or
  10. work with multiple browsers and use one tab in each (less inconvenient, but still not ideal)However, the following code snippets resolve this problem cleanly and effectively: (v2018)1) Add the following to Client Scripts > Global > Pages with header/footer > Startup Script:
if (!window.name)
window.name = ew.random();

$('a').each(function() {
if ($(this).data('toggle') === 'tab')
return;

var url = $(this).attr('href');
var hash = '';

if (!url || url == "#" || url.indexOf('javascript:') > -1)
return;

if (url.indexOf('#') > -1) {
var _url = url.split('#');
url = _url[0];

hash = '#' + _url[1];

if (hash == '#')
hash = '';
}

var _query = url.split('?');

if (_query[1]) {
var query = '?' + _query[1];
var param = {};

query.replace(/(?:\?|&)([^&=]*)=?([^&]*)/g, function ($0, $1, $2) {
if ($1)
param[$1] = $2;
});

param['tab'] = window.name;
_query[1] = '?';

for (var index in param)
_query[1] += index + '=' + param[index] + '&';

_query[1] = _query[1].substr(0, _query[1].length - 1);
url = _query[0] + _query[1];
}
else
url += '?tab=' + window.name;

$(this).attr('href', url + hash);
});

$('form').each(function() {
var action = $(this).attr('action');
var method = $(this).attr('method');

if (!method || method.toLowerCase() == 'post')
$('<input type="hidden" name="tab" value="' + window.name + '" />').prependTo($(this));
else if (action)
$(this).attr('action', action + (action.indexOf('?') > -1 ? '&' : '?') + 'tab=' + window.name);
});

$('div[onmousedown]').each(function() {
var mousedown = $(this).attr('onmousedown');
$(this).attr('onmousedown', mousedown.replace('\',', '&tab=' + window.name + '\','));
});
  1. Update the following phpfn.php functions as shown:
    for v2020:
	// Get session value
	public function getSessionValue()
	{
		return @$_SESSION[PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : "")];
	}

	// Set session value
	public function setSessionValue($v)
	{
		$_SESSION[PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : "")] = $v;
	}

for v2018:

	// Get session value
	public function getSessionValue()
	{
		return @$_SESSION[EW_PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : "")];
	}

	// Set session value
	public function setSessionValue($v)
	{
		$_SESSION[EW_PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : "")] = $v;
	}

** Remember not to overwrite phpfn.php after making the changes!

All links on a new page are SUPPOSED to have the same “tab=xxx” reference.

When you click a link, a new page opens and uses the “tab=xxx” reference within its URL to grab the correct session variables to construct its own links …and each link on that new page will also have the same “tab=xxx” reference, but it will be different to the “tab=xxx” reference on the previous page because the new page has its own set of session variables.

So, if you start on a list page where the links all end with “tab=1”, that means there is a set of session variables with a “_1” suffix.

When you click a link, the new page opens and grabs values from the “_1” suffixed session variables and then creates its own set of session variables with a “_2” suffix, and adds “tab=2” to all of its links so that clicking a link opens a page that grabs values from the “_2” suffixed session variables and then creates its own set of session variables with a “_3” suffix …and so on. The result is that each page has access to the correct parent values which ensures that detail records maintain integrity.

Just a quick note… the getSessionValue() and setSessionValue() functions have been moved from phpfn.php and are now located in /src/DbField.php (v2021)This is how the edited functions should look:

// Get session value
public function getSessionValue()
{
    return Session(PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : ""));
}

// Set session value
public function setSessionValue($v)
{
    $_SESSION[PROJECT_NAME . "_" . $this->TableVar . "_" . $this->FieldVar . "_SessionValue" . (isset($_REQUEST["tab"]) ? "_" . $_REQUEST["tab"] : "")] = $v;
}

Thank you, Adam.

This tip is even still working properly for v2023 (similar to modification for v2021).