Dont show the reply card in ticket until a reply button is clicked

This commit is contained in:
johnnyq
2026-08-29 00:21:40 -04:00
parent 950d02d6ab
commit 087c75f3d7
3 changed files with 166 additions and 88 deletions

View File

@@ -72,3 +72,18 @@
min-width: 45%; min-width: 45%;
} }
} }
/*
* Bootstrap cancels hover on any .btn following a .btn-check (twbs #31149 / PR #37026):
* a checked outline toggle and an unchecked one looked identical while hovered, so you
* could not read the control's state without moving the pointer away. Correct, but the
* selector also catches the UNCHECKED buttons, and with nothing preselected here that
* leaves the whole strip with no feedback at all.
*
* Give the unchecked ones a NEUTRAL tint - not the variant fill, which is what checked
* looks like and would reintroduce exactly the ambiguity #31149 was about. Upstream's
* color and border-color still apply, so only the background moves.
*/
#replyTypePicker .btn-check:not(:checked) + .btn:hover {
background-color: var(--bs-secondary-bg);
}

View File

@@ -1865,9 +1865,10 @@ if (isset($_POST['add_ticket_reply'])) {
// Defaults // Defaults
$send_email = 0; $send_email = 0;
$ticket_reply_id = 0; $ticket_reply_id = 0;
if ($_POST['public_reply_type'] == 1 ){ $public_reply_type = intval($_POST['public_reply_type'] ?? 0);
if ($public_reply_type == 1) {
$ticket_reply_type = 'Public'; $ticket_reply_type = 'Public';
} elseif ($_POST['public_reply_type'] == 2 ) { } elseif ($public_reply_type == 2) {
$ticket_reply_type = 'Public'; $ticket_reply_type = 'Public';
$send_email = 1; $send_email = 1;
} else { } else {

View File

@@ -715,11 +715,15 @@ if (isset($_GET['ticket_id'])) {
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body p-3 d-print-none"> <div class="card-body p-3 d-print-none">
<!-- Who will see this reply, stated in the label rather than left to a colour --> <!--
<div class="btn-group w-100 mb-3" role="group"> * Picking a reply type is what opens the composer below. Nothing is
* preselected, so a public reply is always a deliberate choice, and the
* conversation sits right under this strip until an agent wants to write.
-->
<div class="btn-group w-100" role="group" id="replyTypePicker">
<input class="btn-check" id="public_reply_type_opt0" type="radio" name="public_reply_type" value="0"> <input class="btn-check" id="public_reply_type_opt0" type="radio" name="public_reply_type" value="0">
<label class="btn btn-outline-dark" for="public_reply_type_opt0"><i class="fas fa-fw fa-lock me-1"></i>Internal note</label> <label class="btn btn-outline-dark" for="public_reply_type_opt0"><i class="fas fa-fw fa-lock me-1"></i>Internal note</label>
<input class="btn-check" id="public_reply_type_opt1" type="radio" name="public_reply_type" value="1" checked> <input class="btn-check" id="public_reply_type_opt1" type="radio" name="public_reply_type" value="1">
<label class="btn btn-outline-info" for="public_reply_type_opt1"><i class="fas fa-fw fa-comment me-1"></i>Public reply</label> <label class="btn btn-outline-info" for="public_reply_type_opt1"><i class="fas fa-fw fa-comment me-1"></i>Public reply</label>
<?php if ($contact_email) { ?> <?php if ($contact_email) { ?>
<input class="btn-check" id="public_reply_type_opt2" type="radio" name="public_reply_type" value="2"> <input class="btn-check" id="public_reply_type_opt2" type="radio" name="public_reply_type" value="2">
@@ -727,6 +731,9 @@ if (isset($_GET['ticket_id'])) {
<?php } ?> <?php } ?>
</div> </div>
<div class="collapse" id="replyComposer">
<div class="pt-3">
<?php <?php
/* /*
@@ -821,9 +828,17 @@ if (isset($_GET['ticket_id'])) {
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<button type="submit" id="ticket_add_reply" name="add_ticket_reply" class="btn btn-success w-100 mt-3 mt-md-4"> <div class="d-flex gap-2 mt-3 mt-md-4">
<button type="submit" id="ticket_add_reply" name="add_ticket_reply" class="btn btn-success flex-grow-1">
<i class="fas fa-fw fa-paper-plane me-2"></i>Send <i class="fas fa-fw fa-paper-plane me-2"></i>Send
</button> </button>
<button type="button" id="cancelReply" class="btn btn-light" title="Close without replying">
<i class="fas fa-fw fa-times"></i>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
@@ -1510,6 +1525,53 @@ require_once "../includes/footer.php";
}); });
} }
// Reply composer - the type buttons above are the entry point. The composer stays
// collapsed until one is picked, so the conversation is not pushed down the page and
// a public reply is never the default.
const replyComposer = document.getElementById('replyComposer');
if (replyComposer) {
const replyComposerCollapse = bootstrap.Collapse.getOrCreateInstance(replyComposer, { toggle: false });
const replyTypes = document.querySelectorAll('input[name="public_reply_type"]');
// Only ever opens. Switching Internal -> Public mid-draft must not throw the draft away.
replyTypes.forEach(radio => radio.addEventListener('change', () => replyComposerCollapse.show()));
replyComposer.addEventListener('shown.bs.collapse', function () {
// TinyMCE's autoresize plugin measured the editor while its container was
// display:none, so it sized to nothing. Re-measure now the box is real.
const editor = window.tinymce ? tinymce.get('ticket_reply') : null;
if (editor) {
editor.execCommand('mceAutoResize');
editor.focus();
} else {
const textarea = document.getElementById('ticket_reply');
if (textarea) textarea.focus();
}
});
const cancelReply = document.getElementById('cancelReply');
if (cancelReply) {
cancelReply.addEventListener('click', function () {
replyComposerCollapse.hide();
replyTypes.forEach(radio => { radio.checked = false; });
});
}
// The status select is required and lives inside the collapse. A required control in a
// display:none container blocks submission with nothing on screen to explain it, and the
// form can still be submitted with Enter from a focused reply-type button, so refuse
// outright while the composer is shut.
const replyForm = replyComposer.closest('form');
if (replyForm) {
replyForm.addEventListener('submit', function (e) {
if (!replyComposer.classList.contains('show')) {
e.preventDefault();
}
});
}
}
// Conversation filter - show everything, only what the client can see, or only internal notes // Conversation filter - show everything, only what the client can see, or only internal notes
const replyFilter = document.getElementById('replyFilter'); const replyFilter = document.getElementById('replyFilter');
if (replyFilter) { if (replyFilter) {