diff --git a/app/admin/app-documentation.php b/app/admin/app-documentation.php index cf7ea8a..b260370 100644 --- a/app/admin/app-documentation.php +++ b/app/admin/app-documentation.php @@ -3,200 +3,5 @@ $GLOBALS['page_title'] = $Translation['app documentation']; include(__DIR__ . '/incHeader.php'); -?> - -
-
- -
- -
-

- -This is the Northwind demo app, showing a sample, relatively sophisticated, app that can be created by AppGini. - -

- -

Customers

-

- - - -

- -

Total Sales

-

- -

This is an automatically calculated field that uses this SQL query:

-
SELECT SUM(`order_details`.`UnitPrice` * `order_details`.`Quantity` - `order_details`.`Discount`) FROM `customers` 
-LEFT JOIN `orders` ON `orders`.`CustomerID`=`customers`.`CustomerID` 
-LEFT JOIN `order_details` ON `orders`.`OrderID`=`order_details`.`OrderID` 
-WHERE `customers`.`CustomerID`='%ID%'
- -

-

Employees

-

- - - -

- -

Age

-

- -

This field is Automatically calculated. The SQL query used for calculation is:

- -
SELECT FLOOR(DATEDIFF(NOW(), `employees`.`BirthDate`) / 365) FROM `employees` 
-WHERE `employees`.`EmployeeID`='%ID%'
- -

DATEDIFF returns the difference between 2 dates in days, so we should divide by 365 to get years. Using FLOOR rounds down age to the nearest year ... That is, if someone is 32.4 years old, her age would display as 32. Also, if someone is 36.9 years old, she'd show as 36 years old since she's, technically, not 37 yet ;)

- -

-

Total Sales

-

- -

This field calculates the total sales made by current employee using this SQL query:

- -
SELECT SUM(`order_details`.`UnitPrice` * `order_details`.`Quantity` - `order_details`.`Discount`) FROM `employees` 
-LEFT JOIN `orders` ON `orders`.`EmployeeID`=`employees`.`EmployeeID` 
-LEFT JOIN `order_details` ON `orders`.`OrderID`=`order_details`.`OrderID` 
-WHERE `employees`.`EmployeeID`='%ID%'
- -

-

Orders

-

- - - -

- -

Status

-

- -

This is a calculated field based on the following SQL query

-
SELECT
-IF(
-    `orders`.`ShippedDate`, 
-        '<span class="text-success">Shipped</span>', 
-        /* else */
-        IF(
-           `orders`.`RequiredDate` < now(), 
-                '<span class="text-danger">Late</span>', 
-                /* else */
-                '<span class="text-warning">Pending</span>'
-        )
-) 
-FROM `orders` 
-WHERE `orders`.`OrderID`='%ID%'
-

For late orders, a Late status would be displayed. -For orders not yet shipped, but not late, a Pending status would be displayed. -And for shipped orders, Shipped will be displayed.

- -

-

Total

-

- -

This field is automatically calculated using the following SQL query:

-
SELECT SUM(`order_details`.`UnitPrice` * `order_details`.`Quantity`) + `orders`.`Freight` FROM `orders` 
-LEFT JOIN `order_details` ON `order_details`.`OrderID`=`orders`.`OrderID` 
-WHERE `orders`.`OrderID`='%ID%'
- -

-

Order Items

-

- - - -

- -

Subtotal

-

- -

This field is automatically calculated using the following query:

- -
SELECT `order_details`.`UnitPrice` * `order_details`.`Quantity` - `order_details`.`Discount` FROM `order_details` 
-WHERE `order_details`.`odID`='%ID%'
- -

The formula above calculates the subtotal of this invoice line by multiplying unit price by quantity and then subtracting discount amount.

- -

-

Shippers

-

- - - -

- -

Number of orders shipped

-

- -

This field is Automatically calculated. The SQL query used is:

- -
SELECT COUNT(1) FROM `shippers` 
-LEFT JOIN `orders` ON `orders`.`ShipVia`=`shippers`.`ShipperID` 
-WHERE `shippers`.`ShipperID`='%ID%'
- -

-
-
- - - - - -"; @@ -1638,6 +1642,107 @@ function get_table_fields($tn = null) { return isset($schema[$tn]) ? $schema[$tn] : []; } + ######################################################################## + function updateField($tn, $fn, $dataType, $notNull = false, $default = null, $extra = null) { + $sqlNull = $notNull ? 'NOT NULL' : 'NULL'; + $sqlDefault = $default === null ? '' : "DEFAULT '" . makeSafe($default) . "'"; + $sqlExtra = $extra === null ? '' : $extra; + + // get current field definition + $col = false; + $eo = ['silentErrors' => true]; + $res = sql("SHOW COLUMNS FROM `{$tn}` LIKE '{$fn}'", $eo); + if($res) $col = db_fetch_assoc($res); + + // if field does not exist, create it + if(!$col) { + sql("ALTER TABLE `{$tn}` ADD COLUMN `{$fn}` {$dataType} {$sqlNull} {$sqlDefault} {$sqlExtra}", $eo); + return; + } + + // if field exists, alter it if needed + if( + strtolower($col['Type']) != strtolower($dataType) || + (strtolower($col['Null']) == 'yes' && $notNull) || + (strtolower($col['Null']) == 'no' && !$notNull) || + (strtolower($col['Default']) != strtolower($default)) || + (strtolower($col['Extra']) != strtolower($extra)) + ) { + sql("ALTER TABLE `{$tn}` CHANGE COLUMN `{$fn}` `{$fn}` {$dataType} {$sqlNull} {$sqlDefault} {$sqlExtra}", $eo); + } + } + + ######################################################################## + function addIndex($tn, $fields, $unique = false) { + // if $fields is a string, convert it to an array + if(!is_array($fields)) $fields = [$fields]; + + // reshape fields so that key is field name and value is index length or null for full length + $fields2 = []; + foreach($fields as $k => $v) { + if(is_numeric($k)) { + $fields2[$v] = null; // $v is field name and index length is full length + continue; + } + + $fields2[$k] = $v; // $k is field name and $v is index length + } + unset($fields); $fields = $fields2; + + // prepare index name and sql + $index_name = implode('_', array_keys($fields)); + $sql = "ALTER TABLE `{$tn}` ADD " . ($unique ? 'UNIQUE ' : '') . "INDEX `{$index_name}` ("; + foreach($fields as $field => $length) + $sql .= "`$field`" . ($length === null ? '' : "($length)") . ','; + $sql = rtrim($sql, ',') . ')'; + + // get current indexes + $eo = ['silentErrors' => true]; + $res = sql("SHOW INDEXES FROM `{$tn}`", $eo); + $indexes = []; + while($row = db_fetch_assoc($res)) + $indexes[$row['Key_name']][$row['Seq_in_index']] = $row; + + // if index does not exist, create it + if(!isset($indexes[$index_name])) { + sql($sql, $eo); + return; + } + + // if index exists, alter it if needed + $index = $indexes[$index_name]; + $index_changed = false; + $index_fields = []; + foreach($index as $seq_in_index => $info) + $index_fields[$seq_in_index] = $info['Column_name']; + + if(count($index_fields) != count($fields)) $index_changed = true; + foreach($fields as $field => $length) { + // check if field exists in index + $seq_in_index = array_search($field, $index_fields); + if($seq_in_index === false) { + $index_changed = true; + break; + } + + // check if field length is different + if($length !== null && $length != $index[$seq_in_index]['Sub_part']) { + $index_changed = true; + break; + } + + // check index uniqueness + if(($unique && $index[$seq_in_index]['Non_unique'] == 1) || (!$unique && $index[$seq_in_index]['Non_unique'] == 0)) { + $index_changed = true; + break; + } + } + if(!$index_changed) return; + + sql("ALTER TABLE `{$tn}` DROP INDEX `{$index_name}`", $eo); + sql($sql, $eo); + } + ######################################################################## function update_membership_groups() { $tn = 'membership_groups'; @@ -1655,9 +1760,9 @@ function update_membership_groups() { ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `name` `name` VARCHAR(100) NOT NULL", $eo); - sql("ALTER TABLE `{$tn}` ADD UNIQUE INDEX `name` (`name`)", $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `allowCSVImport` TINYINT NOT NULL DEFAULT '0'", $eo); + updateField($tn, 'name', 'VARCHAR(100)', true); + addIndex($tn, 'name', true); + updateField($tn, 'allowCSVImport', 'TINYINT', true, '0'); } ######################################################################## function update_membership_users() { @@ -1688,14 +1793,14 @@ function update_membership_users() { ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `pass_reset_key` VARCHAR(100)", $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `pass_reset_expiry` INT UNSIGNED", $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `passMD5` `passMD5` VARCHAR(255)", $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `memberID` `memberID` VARCHAR(100) NOT NULL", $eo); - sql("ALTER TABLE `{$tn}` ADD INDEX `groupID` (`groupID`)", $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `flags` TEXT", $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `allowCSVImport` TINYINT NOT NULL DEFAULT '0'", $eo); - sql("ALTER TABLE `{$tn}` ADD COLUMN `data` LONGTEXT", $eo); + updateField($tn, 'pass_reset_key', 'VARCHAR(100)'); + updateField($tn, 'pass_reset_expiry', 'INT UNSIGNED'); + updateField($tn, 'passMD5', 'VARCHAR(255)'); + updateField($tn, 'memberID', 'VARCHAR(100)', true); + addIndex($tn, 'groupID'); + updateField($tn, 'flags', 'TEXT'); + updateField($tn, 'allowCSVImport', 'TINYINT', true, '0'); + updateField($tn, 'data', 'LONGTEXT'); } ######################################################################## function update_membership_userrecords() { @@ -1720,12 +1825,12 @@ function update_membership_userrecords() { ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` ADD UNIQUE INDEX `tableName_pkValue` (`tableName`, `pkValue`(100))", $eo); - sql("ALTER TABLE `{$tn}` ADD INDEX `pkValue` (`pkValue`)", $eo); - sql("ALTER TABLE `{$tn}` ADD INDEX `tableName` (`tableName`)", $eo); - sql("ALTER TABLE `{$tn}` ADD INDEX `memberID` (`memberID`)", $eo); - sql("ALTER TABLE `{$tn}` ADD INDEX `groupID` (`groupID`)", $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `memberID` `memberID` VARCHAR(100)", $eo); + addIndex($tn, ['tableName' => null, 'pkValue' => 100], true); + addIndex($tn, 'pkValue'); + addIndex($tn, 'tableName'); + addIndex($tn, 'memberID'); + addIndex($tn, 'groupID'); + updateField($tn, 'memberID', 'VARCHAR(100)'); } ######################################################################## function update_membership_grouppermissions() { @@ -1745,7 +1850,7 @@ function update_membership_grouppermissions() { ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` ADD UNIQUE INDEX `groupID_tableName` (`groupID`, `tableName`)", $eo); + addIndex($tn, ['groupID', 'tableName'], true); } ######################################################################## function update_membership_userpermissions() { @@ -1765,8 +1870,8 @@ function update_membership_userpermissions() { ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `memberID` `memberID` VARCHAR(100) NOT NULL", $eo); - sql("ALTER TABLE `{$tn}` ADD UNIQUE INDEX `memberID_tableName` (`memberID`, `tableName`)", $eo); + updateField($tn, 'memberID', 'VARCHAR(100)', true); + addIndex($tn, ['memberID', 'tableName'], true); } ######################################################################## function update_membership_usersessions() { @@ -1795,11 +1900,11 @@ function update_membership_cache() { `request` VARCHAR(100) NOT NULL, `request_ts` INT, `response` LONGTEXT, - PRIMARY KEY (`request`)) + PRIMARY KEY (`request`) ) CHARSET " . mysql_charset, $eo); - sql("ALTER TABLE `{$tn}` CHANGE COLUMN `response` `response` LONGTEXT", $eo); + updateField($tn, 'response', 'LONGTEXT'); } ######################################################################## function thisOr($this_val, $or = ' ') { @@ -3149,3 +3254,107 @@ function ctype_alnum($str) { return preg_match('/^[a-zA-Z0-9]+$/', $str); } } + + /** + * Perform an HTTP request and return the response, including headers and body, with support to cookies + * + * @param string $url URL to request + * @param array $payload payload to send with the request + * @param array $headers headers to send with the request, in the format ['header' => 'value'] + * @param string $type request type, either 'GET' or 'POST' + * @param string $cookieJar path to a file to read/store cookies in + * + * @return array response, including `'headers'` and `'body'`, or error info if request failed + */ + function httpRequest($url, $payload = [], $headers = [], $type = 'GET', $cookieJar = null) { + // prep raw headers + if(!isset($headers['User-Agent'])) $headers['User-Agent'] = $_SERVER['HTTP_USER_AGENT']; + if(!isset($headers['Accept'])) $headers['Accept'] = $_SERVER['HTTP_ACCEPT']; + $rawHeaders = []; + foreach($headers as $k => $v) $rawHeaders[] = "$k: $v"; + + $payloadQuery = http_build_query($payload); + + // for GET requests, append payload to url + if($type == 'GET' && strlen($payloadQuery)) $url .= "?$payloadQuery"; + + $respHeaders = []; + $ch = curl_init(); + $options = [ + CURLOPT_URL => $url, + CURLOPT_POST => ($type == 'POST'), + CURLOPT_POSTFIELDS => ($type == 'POST' && strlen($payloadQuery) ? $payloadQuery : null), + CURLOPT_HEADER => false, + CURLOPT_HEADERFUNCTION => function($curl, $header) use (&$respHeaders) { + list($k, $v) = explode(': ', $header); + $respHeaders[trim($k)] = trim($v); + return strlen($header); + }, + CURLOPT_HTTPHEADER => $rawHeaders, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_RETURNTRANSFER => true, + ]; + + /* if this is a localhost request, no need to verify SSL */ + if(preg_match('/^https?:\/\/(localhost|127\.0\.0\.1)/i', $url)) { + $options[CURLOPT_SSL_VERIFYPEER] = false; + $options[CURLOPT_SSL_VERIFYHOST] = false; + } + + if($cookieJar) { + $options[CURLOPT_COOKIEJAR] = $cookieJar; + $options[CURLOPT_COOKIEFILE] = $cookieJar; + } + + if(defined('CURLOPT_TCP_FASTOPEN')) $options[CURLOPT_TCP_FASTOPEN] = true; + if(defined('CURLOPT_UNRESTRICTED_AUTH')) $options[CURLOPT_UNRESTRICTED_AUTH] = true; + + curl_setopt_array($ch, $options); + + $respBody = curl_exec($ch); + + if($respBody === false) return [ + 'error' => curl_error($ch), + 'info' => curl_getinfo($ch), + ]; + + curl_close($ch); + + // wait for 0.05 seconds after launching request + usleep(50000); + + return [ + 'headers' => $respHeaders, + 'body' => $respBody, + ]; + } + + /** + * @brief Retrieve owner username of the record with the given primary key value + * + * @param $tn string table name + * @param $pkValue string primary key value + * @return string|null username of the record owner, or null if not found + */ + function getRecordOwner($tn, $pkValue) { + $tn = makeSafe($tn); + $pkValue = makeSafe($pkValue); + $owner = sqlValue("SELECT `memberID` FROM `membership_userrecords` WHERE `tableName`='{$tn}' AND `pkValue`='$pkValue'"); + + if(!strlen($owner)) return null; + return $owner; + } + + /** + * @brief Retrieve lookup field name that determines record owner of the given table + * + * @param $tn string table name + * @return string|null lookup field name, or null if default (record owner is user that creates the record) + */ + function tableRecordOwner($tn) { + $owners = [ + ]; + + return $owners[$tn] ?? null; + } + diff --git a/app/admin/incHeader.php b/app/admin/incHeader.php index 5a29d00..677780a 100644 --- a/app/admin/incHeader.php +++ b/app/admin/incHeader.php @@ -203,10 +203,10 @@ function hideDialogs() {
  • -
  • +
  • -
  • +
  • @@ -217,21 +217,36 @@ function hideDialogs() { - - - + + + diff --git a/app/ajax_combo.php b/app/ajax_combo.php index 74e5e8b..5f42d0a 100644 --- a/app/ajax_combo.php +++ b/app/ajax_combo.php @@ -1,5 +1,5 @@ Query)) { - // do we have an order by? - if(preg_match('/\border by\b/i', $combo->Query)) - $combo->Query = preg_replace('/\b(order by)\b/i', ' GROUP BY 2 $1', $combo->Query); - else - $combo->Query .= ' GROUP BY 2 '; - } - if(!preg_match('/ limit .+/i', $combo->Query)) { if(!$search_id) $combo->Query .= " LIMIT {$skip}, {$results_per_page}"; if($search_id) $combo->Query .= " LIMIT 1"; @@ -374,12 +365,25 @@ $eo = ['silentErrors' => true]; + /* + if unique text (ut=1), we don't care about IDs and can group by text. + initiate array to hold unique texts. + */ + if($uniqueText) $uniqueArr = []; + $res = sql($combo->Query, $eo); if($res) while($row = db_fetch_row($res)) { + // Add initial empty value if field is not required and this is the first page of results if(empty($prepared_data) && $page == 1 && !$search_id && !$field['not_null']) { $prepared_data[] = ['id' => empty_lookup_value, 'text' => to_utf8("<{$Translation['none']}>")]; } + // if unique text, add to uniqueArr and skip if already exists + if($uniqueText) { + if(in_array($row[1], $uniqueArr)) continue; + $uniqueArr[] = $row[1]; + } + $prepared_data[] = ['id' => to_utf8($row[0]), 'text' => to_utf8($xss->xss_clean($row[1]))]; } } diff --git a/app/categories_autofill.php b/app/categories_autofill.php index 3207ce0..0f4b73b 100644 --- a/app/categories_autofill.php +++ b/app/categories_autofill.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = 1; + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 5; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/common.js b/app/common.js index eefd24f..1ae3582 100644 --- a/app/common.js +++ b/app/common.js @@ -1,6 +1,6 @@ var AppGini = AppGini || {}; -AppGini.version = 23.16; +AppGini.version = 24.11; /* initials and fixes */ jQuery(function() { @@ -310,6 +310,25 @@ jQuery(function() { condition: () => AppGini.Translate !== undefined, action: () => moment.locale(AppGini.Translate._map['datetimepicker locale']) }) + + // if upload toolbox is empty, hide it + $j('.upload-toolbox').toggleClass('hidden', !$j('.upload-toolbox').children().not('.hidden').length) + + // on clicking .sql-query-copier or .sql-query-container, copy the query to clipboard + // and set .sql-query-copier to 'copied' for 1 second + $j(document).on('click', '.sql-query-copier', function() { + const query = $j(this).siblings('.sql-query-container').text().trim(); + if(!query) return; + + AppGini.copyToClipboard(query); + + $j(this).text(AppGini.Translate._map['copied']); + setTimeout(() => $j(this).text(AppGini.Translate._map['click to copy']), 1000); + }) + $j(document).on('click', '.sql-query-container', function() { + $j(this).siblings('.sql-query-copier').click(); + }) + }); /* show/hide TV action buttons based on whether records are selected or not */ @@ -1875,6 +1894,11 @@ AppGini.sortSelect2ByRelevence = function(res, cont, qry) { if(aStart && !bStart) return false; if(!aStart && bStart) return true; } + + // if trimmed item is empty, always return it first + if(a.text.trim() == '') return false; + if(b.text.trim() == '') return true; + return a.text > b.text; }); } @@ -1895,6 +1919,12 @@ AppGini.alterDVTitleLinkToBack = function() { }) } +AppGini.isRecordUpdated = () => { + var url = new URL(window.location.href); + var params = new URLSearchParams(url.search); + return params.has('record-updated-ok') || params.has('record-added-ok'); +} + AppGini.lockUpdatesOnUserRequest = function() { // if this is not DV of existing record where editing and saving a copy are both enabled, skip if(!$j('#update').length || !$j('#insert').length || !$j('input[name=SelectedID]').val().length) return; @@ -1926,6 +1956,9 @@ AppGini.lockUpdatesOnUserRequest = function() { locker.toggleClass('active'); locker.prop('title', AppGini.Translate._map[locker.hasClass('active') ? 'Enable' : 'Disable']); }) + + // if record has just been added/updated, lock updates + if(AppGini.isRecordUpdated()) $j('.btn-update-locker').trigger('click'); } /* function to focus a specific element of a form, given field name */ @@ -2518,3 +2551,23 @@ AppGini.updateChildrenCount = (scheduleNextCall = true) => { setTimeout(AppGini.updateChildrenCount, elapsed > 2000 ? 60000 : 10000); }); } + +AppGini.copyToClipboard = (text) => { + if(navigator.clipboard) { + navigator.clipboard.writeText(text); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.select(); + document.execCommand('copy'); + document.body.removeChild(textArea); +} + +AppGini.htmlEntitiesToText = (html) => { + const txt = document.createElement('textarea'); + txt.innerHTML = html; + return txt.value; +} diff --git a/app/customers_autofill.php b/app/customers_autofill.php index 497e7fa..91aa26b 100644 --- a/app/customers_autofill.php +++ b/app/customers_autofill.php @@ -1,5 +1,5 @@ $newID], ['pkValue' => $selected_id, 'tableName' => 'customers']); + $selected_id = $newID; + } } function customers_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $AllowDelete = 1, $separateDV = 0, $TemplateDV = '', $TemplateDVP = '') { @@ -297,7 +302,7 @@ function customers_form($selected_id = '', $AllowUpdate = 1, $AllowInsert = 1, $ $filterField = Request::val('FilterField'); $filterOperator = Request::val('FilterOperator'); $filterValue = Request::val('FilterValue'); - $combo_Country->SelectedText = (isset($filterField[1]) && $filterField[1] == '9' && $filterOperator[1] == '<=>' ? $filterValue[1] : ''); + $combo_Country->SelectedText = (isset($filterField[1]) && $filterField[1] == '9' && $filterOperator[1] == '<=>' ? $filterValue[1] : entitiesToUTF8('')); } $combo_Country->Render(); diff --git a/app/customers_view.php b/app/customers_view.php index 26c0359..11ceac6 100644 --- a/app/customers_view.php +++ b/app/customers_view.php @@ -1,5 +1,5 @@ AllowPrinting = 1; $x->AllowPrintingDV = 1; $x->AllowCSV = (getLoggedAdmin() !== false); + $x->AllowAdminShowSQL = 0; $x->RecordsPerPage = 10; $x->QuickSearch = 1; $x->QuickSearchText = $Translation['quick search']; diff --git a/app/datalist.php b/app/datalist.php index e3f9ec2..4b602f0 100644 --- a/app/datalist.php +++ b/app/datalist.php @@ -48,6 +48,7 @@ class DataList { $AllowPrintingDV, $HideTableView, $AllowCSV, + $AllowAdminShowSQL, $CSVSeparator, $QuickSearch, // 0 to 3 @@ -96,6 +97,7 @@ function DataList() { // Constructor function $this->HideTableView = 0; $this->QuickSearch = 0; $this->AllowCSV = 0; + $this->AllowAdminShowSQL = 0; $this->CSVSeparator = ','; $this->AllowDVNavigation = true; @@ -296,6 +298,9 @@ function Render() { $url .= '&SelectedID=' . urlencode($SelectedID); } + // append browser window id to url + $url .= (strpos($url, '?') === false ? '?' : '&') . WindowMessages::windowIdQuery(); + @header('Location: ' . $url); $this->HTML .= ""; @@ -357,6 +362,10 @@ function Render() { $filtersGET = substr($filtersGET, 1); // remove initial & $redirectUrl = $this->ScriptFileName . '?SelectedID=' . urlencode($SelectedID) . '&' . $filtersGET . '&' . $update_status; + + // append browser window id to url + $redirectUrl .= (strpos($redirectUrl, '?') === false ? '?' : '&') . WindowMessages::windowIdQuery(); + @header("Location: $redirectUrl"); $this->HTML .= ''; return; @@ -709,7 +718,7 @@ function Render() { if($current_view == 'DV' && !$Embedded) { $this->HTML .= '