mirror of
https://github.com/itflow-org/itflow
synced 2026-09-05 22:35:12 +00:00
Feature: Add Ticket Canned Responses
This commit is contained in:
@@ -382,6 +382,37 @@ if (isset($_GET['get_active_clients'])) {
|
||||
echo json_encode($response);
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns the body of one canned response, for the picker on the ticket reply form.
|
||||
* Fetched on demand rather than rendered into every ticket page, because the bodies are
|
||||
* full HTML replies and an install can have a lot of them.
|
||||
*/
|
||||
if (isset($_GET['get_canned_response'])) {
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
$canned_response_id = intval($_GET['get_canned_response']);
|
||||
|
||||
$canned_sql = mysqli_query($mysqli, "SELECT canned_response_body FROM canned_responses
|
||||
WHERE canned_response_id = $canned_response_id AND canned_response_archived_at IS NULL LIMIT 1");
|
||||
|
||||
$canned_row = mysqli_fetch_assoc($canned_sql);
|
||||
|
||||
// Purified here rather than at save time, the same way ticket replies and ticket
|
||||
// template details are - what lands in the editor is what would have been rendered
|
||||
require_once "../libs/htmlpurifier/HTMLPurifier.standalone.php";
|
||||
|
||||
$canned_purifier_config = HTMLPurifier_Config::createDefault();
|
||||
$canned_purifier_config->set('Cache.DefinitionImpl', null);
|
||||
$canned_purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
|
||||
$canned_purifier = new HTMLPurifier($canned_purifier_config);
|
||||
|
||||
$response = [];
|
||||
$response['body'] = $canned_row ? $canned_purifier->purify($canned_row['canned_response_body']) : '';
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns ordered list of active contacts for a specified client
|
||||
*/
|
||||
|
||||
@@ -727,8 +727,57 @@ if (isset($_GET['ticket_id'])) {
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Canned responses offered on this ticket: the ones tied to its
|
||||
* category, plus the general ones that are offered everywhere.
|
||||
* Names only - the body is fetched when one is picked, so a
|
||||
* shelf of long responses does not ride along with every ticket.
|
||||
*/
|
||||
$sql_canned_responses = mysqli_query($mysqli, "SELECT canned_response_id, canned_response_name, canned_response_category_id
|
||||
FROM canned_responses
|
||||
WHERE canned_response_archived_at IS NULL
|
||||
AND (canned_response_category_id = 0 OR canned_response_category_id = $ticket_category)
|
||||
ORDER BY canned_response_name ASC");
|
||||
|
||||
$canned_responses_for_category = [];
|
||||
$canned_responses_general = [];
|
||||
|
||||
while ($canned_row = mysqli_fetch_assoc($sql_canned_responses)) {
|
||||
if (intval($canned_row['canned_response_category_id']) === 0) {
|
||||
$canned_responses_general[] = $canned_row;
|
||||
} else {
|
||||
$canned_responses_for_category[] = $canned_row;
|
||||
}
|
||||
}
|
||||
|
||||
if ($canned_responses_for_category || $canned_responses_general) { ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<select class="form-select" id="canned_response_picker">
|
||||
<option value="">- Insert a canned response -</option>
|
||||
<?php if ($canned_responses_for_category) { ?>
|
||||
<optgroup label="<?= $ticket_category_display ?>">
|
||||
<?php foreach ($canned_responses_for_category as $canned_row) { ?>
|
||||
<option value="<?= intval($canned_row['canned_response_id']) ?>"><?= escapeHtml($canned_row['canned_response_name']) ?></option>
|
||||
<?php } ?>
|
||||
</optgroup>
|
||||
<?php } ?>
|
||||
<?php if ($canned_responses_general) { ?>
|
||||
<optgroup label="All categories">
|
||||
<?php foreach ($canned_responses_general as $canned_row) { ?>
|
||||
<option value="<?= intval($canned_row['canned_response_id']) ?>"><?= escapeHtml($canned_row['canned_response_name']) ?></option>
|
||||
<?php } ?>
|
||||
</optgroup>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<textarea class="form-control tinymceTicket" name="ticket_reply" placeholder="Type a response"></textarea>
|
||||
<textarea class="form-control tinymceTicket" id="ticket_reply" name="ticket_reply" placeholder="Type a response"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -1424,6 +1473,43 @@ require_once "../includes/footer.php";
|
||||
});
|
||||
}
|
||||
|
||||
// Canned responses - insert at the cursor rather than replacing, so picking one into a
|
||||
// half-written reply adds to it. The body is fetched on demand; TinyMCE owns the field
|
||||
// by now, so setting the textarea value directly would do nothing visible.
|
||||
const cannedPicker = document.getElementById('canned_response_picker');
|
||||
if (cannedPicker) {
|
||||
cannedPicker.addEventListener('change', function () {
|
||||
const cannedId = cannedPicker.value;
|
||||
if (!cannedId) return;
|
||||
|
||||
cannedPicker.disabled = true;
|
||||
|
||||
fetch('ajax.php?get_canned_response=' + encodeURIComponent(cannedId))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const editor = window.tinymce ? tinymce.get('ticket_reply') : null;
|
||||
|
||||
if (editor) {
|
||||
editor.insertContent(data.body);
|
||||
editor.focus();
|
||||
} else {
|
||||
// TinyMCE failed to load - fall back to the plain textarea
|
||||
const textarea = document.getElementById('ticket_reply');
|
||||
if (textarea) {
|
||||
textarea.value += data.body;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Could not load that canned response.');
|
||||
})
|
||||
.finally(() => {
|
||||
cannedPicker.disabled = false;
|
||||
cannedPicker.value = '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Conversation filter - show everything, only what the client can see, or only internal notes
|
||||
const replyFilter = document.getElementById('replyFilter');
|
||||
if (replyFilter) {
|
||||
|
||||
Reference in New Issue
Block a user