mirror of
https://github.com/itflow-org/itflow
synced 2026-09-03 05:15:12 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b982dfe2d1 | ||
|
|
f1ea055f51 | ||
|
|
da28d285fb | ||
|
|
70216f97b9 | ||
|
|
9e8c4d002a | ||
|
|
0c690c4e51 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -2,6 +2,31 @@
|
|||||||
|
|
||||||
This file documents all notable changes made to ITFlow.
|
This file documents all notable changes made to ITFlow.
|
||||||
|
|
||||||
|
## [26.09.2] Maint Release
|
||||||
|
|
||||||
|
- Updates the App Version to a proper version number.
|
||||||
|
|
||||||
|
|
||||||
|
## [26.09.1] Maint Release
|
||||||
|
|
||||||
|
### Upgrading to 26.09.1
|
||||||
|
|
||||||
|
Update from Maintenance > Update — Queue Update hands the job to cron and it applies on its own. There is no database change in this release, so nothing else is required.
|
||||||
|
|
||||||
|
### Breaking Changes and Notes
|
||||||
|
|
||||||
|
- API: a contact must be archived before the delete endpoint will remove it. Deleting an active contact is refused and reports nothing deleted, so archive it first and then delete. Thanks to @Wrongecho.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Assets: IP address fields demanded all three digits of every octet, so `10.0.0.1` had to be entered as `010.000.000.001`. They take natural input again, and a field holding DHCP is now left alone rather than being emptied the moment the modal opens.
|
||||||
|
- Networks: in the IP list, an empty hostname or description shows a dash rather than a blank cell, the column headings match the rest of the app, and the table is tighter so more addresses fit on screen.
|
||||||
|
|
||||||
|
### Developer Updates
|
||||||
|
|
||||||
|
- The IPv4 mask in `js/app.js` is a regex mask rather than four `IMask.MaskedRange` blocks. A pattern mask will not advance past a separator until the current block reaches its `maxLength`, which is what forced the three-digit octets. A regex mask has no per-block completeness rule and tests the whole value on each keystroke, so a partial `10.0.` is valid on its own. Octets are still bounded to 0-255 and leading zeros are still accepted, matching what the old jquery.inputmask `ip` alias allowed. Any value not made purely of digits and dots is skipped, because `interface_ip` and `asset_ip` are `varchar(200)` and also carry the literal `DHCP` written by the checkbox on those same modals.
|
||||||
|
- Dead display variables removed from the asset, expense and product listings. `$asset_description_display`, `$client_name_display` and `$product_description_display` are folded into `?: '-'` at the point of assignment.
|
||||||
|
|
||||||
## [26.09]
|
## [26.09]
|
||||||
|
|
||||||
### Upgrading to 26.09
|
### Upgrading to 26.09
|
||||||
|
|||||||
@@ -556,11 +556,6 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
$asset_type = escapeHtml($row['asset_type']);
|
$asset_type = escapeHtml($row['asset_type']);
|
||||||
$asset_name = escapeHtml($row['asset_name']);
|
$asset_name = escapeHtml($row['asset_name']);
|
||||||
$asset_description = escapeHtml($row['asset_description']);
|
$asset_description = escapeHtml($row['asset_description']);
|
||||||
if ($asset_description) {
|
|
||||||
$asset_description_display = $asset_description;
|
|
||||||
} else {
|
|
||||||
$asset_description_display = "-";
|
|
||||||
}
|
|
||||||
$asset_make = escapeHtml($row['asset_make']);
|
$asset_make = escapeHtml($row['asset_make']);
|
||||||
$asset_model = escapeHtml($row['asset_model']);
|
$asset_model = escapeHtml($row['asset_model']);
|
||||||
$asset_serial = escapeHtml($row['asset_serial']);
|
$asset_serial = escapeHtml($row['asset_serial']);
|
||||||
|
|||||||
@@ -268,12 +268,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
$category_name = escapeHtml($row['category_name']);
|
$category_name = escapeHtml($row['category_name']);
|
||||||
$account_name = escapeHtml($row['account_name']);
|
$account_name = escapeHtml($row['account_name']);
|
||||||
$expense_account_id = intval($row['expense_account_id']);
|
$expense_account_id = intval($row['expense_account_id']);
|
||||||
$client_name = escapeHtml($row['client_name']);
|
$client_name = escapeHtml($row['client_name']) ?: '-';
|
||||||
if(empty($client_name)) {
|
|
||||||
$client_name_display = "-";
|
|
||||||
} else {
|
|
||||||
$client_name_display = $client_name;
|
|
||||||
}
|
|
||||||
$expense_client_id = intval($row['expense_client_id']);
|
$expense_client_id = intval($row['expense_client_id']);
|
||||||
|
|
||||||
if (empty($expense_receipt)) {
|
if (empty($expense_receipt)) {
|
||||||
@@ -307,7 +302,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
<td><?= $vendor_name ?></td>
|
<td><?= $vendor_name ?></td>
|
||||||
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?></td>
|
<td class="text-end font-monospace"><?= numfmt_format_currency($currency_format, $expense_amount, $expense_currency_code) ?></td>
|
||||||
<td><?= $account_name ?></td>
|
<td><?= $account_name ?></td>
|
||||||
<td><?= $client_name_display ?></td>
|
<td><?= $client_name ?></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="dropdown dropstart text-center">
|
<div class="dropdown dropstart text-center">
|
||||||
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
<button class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown">
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
<form id="bulkActions" action="post.php" method="post">
|
<form id="bulkActions" action="post.php" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
|
||||||
<table class="table table-striped table-borderless table-hover mb-0">
|
<table class="table table-striped table-borderless table-hover table-sm mb-0">
|
||||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="checkbox-column border-end">
|
<td class="checkbox-column border-end">
|
||||||
@@ -233,17 +233,17 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_address&order=<?= $disp ?>">
|
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_address&order=<?= $disp ?>">
|
||||||
IP Address <?php if ($sort == 'ip_address') { echo $order_icon; } ?>
|
IP Address <?php if ($sort == 'ip_address') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_hostname&order=<?= $disp ?>">
|
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_hostname&order=<?= $disp ?>">
|
||||||
Hostname <?php if ($sort == 'ip_hostname') { echo $order_icon; } ?>
|
Hostname <?php if ($sort == 'ip_hostname') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=ip_description&order=<?= $disp ?>">
|
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ip_description&order=<?= $disp ?>">
|
||||||
Description <?php if ($sort == 'ip_description') { echo $order_icon; } ?>
|
Description <?php if ($sort == 'ip_description') { echo $order_icon; } ?>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
@@ -256,8 +256,8 @@ if (mysqli_num_rows($sql) == 0) {
|
|||||||
while ($row = mysqli_fetch_assoc($sql_ips)) {
|
while ($row = mysqli_fetch_assoc($sql_ips)) {
|
||||||
$ip_id = intval($row['ip_id']);
|
$ip_id = intval($row['ip_id']);
|
||||||
$ip_address = escapeHtml($row['ip_address']);
|
$ip_address = escapeHtml($row['ip_address']);
|
||||||
$ip_hostname = escapeHtml($row['ip_hostname']);
|
$ip_hostname = escapeHtml($row['ip_hostname']) ?: '-';
|
||||||
$ip_description = escapeHtml($row['ip_description']);
|
$ip_description = escapeHtml($row['ip_description']) ?: '-';
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -232,12 +232,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
while ($row = mysqli_fetch_assoc($sql)) {
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
$product_id = intval($row['product_id']);
|
$product_id = intval($row['product_id']);
|
||||||
$product_name = escapeHtml($row['product_name']);
|
$product_name = escapeHtml($row['product_name']);
|
||||||
$product_description = escapeHtml($row['product_description']);
|
$product_description = escapeHtml($row['product_description']) ?: '-';
|
||||||
if (empty($product_description)) {
|
|
||||||
$product_description_display = "-";
|
|
||||||
} else {
|
|
||||||
$product_description_display = "<div style='white-space:pre-line'>$product_description</div>";
|
|
||||||
}
|
|
||||||
$product_qty = intval($row['product_qty']);
|
$product_qty = intval($row['product_qty']);
|
||||||
$product_code = escapeHtml($row['product_code']);
|
$product_code = escapeHtml($row['product_code']);
|
||||||
$product_location = escapeHtml($row['product_location']) ?: '-';
|
$product_location = escapeHtml($row['product_location']) ?: '-';
|
||||||
@@ -267,7 +262,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td><?= $category_name ?></td>
|
<td><?= $category_name ?></td>
|
||||||
<td><?= $product_description_display ?></td>
|
<td><div style='white-space:pre-line'><?= $product_description ?></div></td>
|
||||||
<?php if ($type_filter == 'product') { ?>
|
<?php if ($type_filter == 'product') { ?>
|
||||||
<td><?= $product_qty ?></td>
|
<td><?= $product_qty ?></td>
|
||||||
<td><?= $product_location ?></td>
|
<td><?= $product_location ?></td>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ $contact_id = intval($_POST['contact_id']);
|
|||||||
$delete_count = false;
|
$delete_count = false;
|
||||||
|
|
||||||
if (!empty($contact_id)) {
|
if (!empty($contact_id)) {
|
||||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1"));
|
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_name FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id AND contact_archived_at IS NOT NULL LIMIT 1"));
|
||||||
$contact_name = escapeSql($row['contact_name'] ?? '');
|
$contact_name = escapeSql($row['contact_name'] ?? '');
|
||||||
|
|
||||||
$delete_sql = mysqli_query($mysqli, "DELETE FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id LIMIT 1");
|
$delete_sql = mysqli_query($mysqli, "DELETE FROM contacts WHERE contact_id = $contact_id AND contact_client_id = $client_id AND contact_archived_at IS NOT NULL LIMIT 1");
|
||||||
|
|
||||||
// Check delete & get affected rows
|
// Check delete & get affected rows
|
||||||
if ($delete_sql && !empty($contact_name)) {
|
if ($delete_sql && !empty($contact_name)) {
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
|
* Update this file each time we merge develop into master. Format is YY.MM (add a .v if there is more than one release a month.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
DEFINE("APP_VERSION", "26.09");
|
DEFINE("APP_VERSION", "26.09.2");
|
||||||
|
|||||||
27
js/app.js
27
js/app.js
@@ -568,15 +568,26 @@ function itflowInit() {
|
|||||||
el.dataset.imaskReady = '1';
|
el.dataset.imaskReady = '1';
|
||||||
var spec = el.getAttribute('data-inputmask') || '';
|
var spec = el.getAttribute('data-inputmask') || '';
|
||||||
if (spec.indexOf('ip') !== -1) {
|
if (spec.indexOf('ip') !== -1) {
|
||||||
|
// A pattern mask built from MaskedRange blocks will not advance past the dot
|
||||||
|
// until the block reaches its maxLength, so 10.0.0.1 had to be entered as
|
||||||
|
// 010.000.000.001. A regex mask has no per-block completeness rule - it tests
|
||||||
|
// the whole value on every keystroke, so partial input like "10.0." is valid on
|
||||||
|
// its own. Octets are still bounded to 0-255 and leading zeros are still
|
||||||
|
// accepted, matching what jquery.inputmask's 'ip' alias allowed.
|
||||||
|
//
|
||||||
|
// interface_ip / asset_ip are varchar(200) and hold free text as well - 'DHCP'
|
||||||
|
// is written there by the checkbox on these same modals. Masking a value like
|
||||||
|
// that would strip it to nothing the moment the modal opened, so anything not
|
||||||
|
// made of digits and dots is left unmasked instead.
|
||||||
|
if (el.value && !/^[\d.]*$/.test(el.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var octet = '(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)';
|
||||||
|
var octet4 = octet + '?';
|
||||||
|
var octet3 = '(' + octet + '(\\.' + octet4 + ')?)?';
|
||||||
|
var octet2 = '(' + octet + '(\\.' + octet3 + ')?)?';
|
||||||
IMask(el, {
|
IMask(el, {
|
||||||
mask: 'a.b.c.d',
|
mask: new RegExp('^(' + octet + '(\\.' + octet2 + ')?)?$')
|
||||||
blocks: {
|
|
||||||
a: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
|
||||||
b: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
|
||||||
c: { mask: IMask.MaskedRange, from: 0, to: 255 },
|
|
||||||
d: { mask: IMask.MaskedRange, from: 0, to: 255 }
|
|
||||||
},
|
|
||||||
lazy: true
|
|
||||||
});
|
});
|
||||||
} else if (spec.indexOf('mac') !== -1) {
|
} else if (spec.indexOf('mac') !== -1) {
|
||||||
IMask(el, {
|
IMask(el, {
|
||||||
|
|||||||
Reference in New Issue
Block a user