ticket history and tags

This commit is contained in:
tp_dhu 2025-05-10 15:26:21 +01:00
parent 916657e09c
commit 0bc558d4d7
8 changed files with 387 additions and 135 deletions

View File

@ -36,7 +36,20 @@ class TicketController extends BaseController implements CRUD {
$ticket_id = $f3->get('PARAMS.id');
$ticket_mapper = new Ticket($this->getDB());
$ticket = $ticket_mapper->findById($ticket_id);
if(!$ticket){
$this->f3->set('SESSION.error', 'Ticket not found');
$this->f3->reroute('/tickets');
return;
}
$assigned_user = $ticket->getAssignedUser();
$ticket_history = $ticket->getHistory();
$map_statuses = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id');
$map_priorities = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id');
$map_users = array_column($this->getDB()->exec('SELECT id, display_name FROM users'), 'display_name', 'id');
// render
$this->renderView('views/ticket/view.html', [
@ -46,7 +59,13 @@ class TicketController extends BaseController implements CRUD {
'comments' => $ticket->comments(),
'parent_tickets' => $ticket->getParentTickets(),
'child_tickets' => $ticket->getChildTickets(),
'ticket_meta' => $ticket->getMetaAssoc()
'ticket_meta' => $ticket->getMetaAssoc(),
'ticket_history' => $ticket_history,
'map' => [
'statuses' => $map_statuses,
'priorities' => $map_priorities,
'users' => $map_users
]
]);
}
@ -58,6 +77,9 @@ class TicketController extends BaseController implements CRUD {
$priorities = (new TicketPriority($db))->findAll();
$statuses = (new TicketStatus($db))->findAll();
$all_tags_model = new \Tag($this->getDB());
$all_tags = $all_tags_model->find([], ['order' => 'name ASC']); // get all tags
// TODO: this needs moving into a model?
$users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC');
$users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users);
@ -66,7 +88,8 @@ class TicketController extends BaseController implements CRUD {
$this->renderView('views/ticket/create.html',[
'priorities' => $priorities,
'statuses' => $statuses,
'users' => $users
'users' => $users,
'all_tags' => $all_tags
]);
}
@ -83,18 +106,29 @@ class TicketController extends BaseController implements CRUD {
'description' => $this->f3->get('POST.description'),
'priority_id' => $this->f3->get('POST.priority_id'),
'status_id' => $this->f3->get('POST.status_id'),
'created_by' => $this->f3->get('SESSION.user.id')
'created_by' => $this->f3->get('SESSION.user.id'),
'assigned_to' => $this->f3->get('POST.assigned_to') == '-1' ? null : $this->f3->get('POST.assigned_to')
];
$ticket_mapper = new Ticket($this->getDB());
$new_ticket_id = $ticket_mapper->createTicket($data);
// custom field
$meta_keys = $this->f3->get('POST.meta_key');
$meta_values = $this->f3->get('POST.meta_value');
$meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values);
$ticket_mapper->setCustomFields($meta_assoc);
// $meta_keys = $this->f3->get('POST.meta_key');
// $meta_values = $this->f3->get('POST.meta_value');
// $meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values);
// $ticket_mapper->setCustomFields($meta_assoc);
$new_ticket = $ticket_mapper->findById($new_ticket_id);
if($new_ticket){
// TAG handling for create
$posted_tags = $this->f3->get('POST.tags');
if(!empty($posted_tags) && is_array($posted_tags)){
$new_ticket->setTags($posted_tags);
}
}
$this->f3->set('SESSION.message', 'Ticket #' . $new_ticket_id . ' created successfully.');
$this->f3->reroute('/ticket/' . $new_ticket_id);
}
@ -120,16 +154,29 @@ class TicketController extends BaseController implements CRUD {
// dropdowns
$priorities = (new TicketPriority($this->getDB()))->findAll();
$statuses = (new TicketStatus($this->getDB()))->findAll();
$all_tags_model = new \Tag($this->getDB());
$all_tags = $all_tags_model->find([], ['order' => 'name ASC']);
// TODO: this needs moving into a model?
$users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC');
$users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users);
// paradox - empty($ticket->tags) was returning true, when there were items present
$current_ticket_tag_ids = [];
if(count($ticket->tags) > 0){
foreach($ticket->tags as $current_tag_data){
$current_ticket_tag_ids[] = $current_tag_data['id'];
}
}
$this->renderView('views/ticket/edit.html',[
'ticket' => $ticket,
'ticket_meta' => $ticket->getMeta(),
'priorities' => $priorities,
'statuses' => $statuses,
'users' => $users
'users' => $users,
'all_tags' => $all_tags,
'current_ticket_tag_ids' => $current_ticket_tag_ids
]
);
return;
@ -142,32 +189,40 @@ class TicketController extends BaseController implements CRUD {
$this->requireLogin();
$this->checkCSRF($f3, '/ticket/create');
$ticket_id = $this->f3->get('PARAMS.id');
$ticket_id = $f3->get('PARAMS.id');
$ticket_mapper = new Ticket($this->getDB());
$ticket = $ticket_mapper->findById($ticket_id);
if(!$ticket){
$this->f3->set('SESSION.error', 'Ticket not found.');
$this->f3->reroute('/tickets');
$f3->set('SESSION.error', 'Ticket not found.');
$f3->reroute('/tickets');
}
$data = [
'title' => $this->f3->get('POST.title'),
'created_at' => $this->f3->get('POST.created_at'),
'description' => $this->f3->get('POST.description'),
'priority_id' => $this->f3->get('POST.priority_id'),
'status_id' => $this->f3->get('POST.status_id'),
'updated_by' => $this->f3->get('SESSION.user.id') ,
'assigned_to' => $this->f3->get('POST.assigned_to') ?: null
'title' => $f3->get('POST.title'),
'created_at' => $f3->get('POST.created_at'),
'description' => $f3->get('POST.description'),
'priority_id' => $f3->get('POST.priority_id'),
'status_id' => $f3->get('POST.status_id'),
'updated_by' => $f3->get('SESSION.user.id') ,
'assigned_to' => $f3->get('POST.assigned_to') == -1 ? null : $f3->get('POST.assigned_to')
];
$ticket->updateTicket($data);
// deal with meta data / custom fields
$meta_keys = $this->f3->get('POST.meta_key');
$meta_values = $this->f3->get('POST.meta_value');
$meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values);
$ticket->setCustomFields($meta_assoc);
// $meta_keys = $this->f3->get('POST.meta_key');
// $meta_values = $this->f3->get('POST.meta_value');
// $meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values);
// $ticket->setCustomFields($meta_assoc);
$posted_tags = $f3->get('POST.tags');
if(is_array($posted_tags)){
$ticket->setTags($posted_tags);
} elseif (empty($posted_tags)){
$ticket->setTags([]);
}
$f3->set('SESSION.message', 'Ticket #' . $ticket_id . ' updated successfully.') ;
$f3->reroute('/ticket/' . $ticket_id);
}

View File

@ -14,7 +14,11 @@ class BulmaFormHelper extends \Prefab {
static public function render($node) {
$attr = $node['@attrib'] ?? [];
$type = strtoupper($attr['type']) ?? null;
if(isset($attr['type'])){
$type = strtoupper($attr['type']);
} else {
$type = null;
}
// all *
$label = $attr['label'] ?? '';

View File

@ -46,8 +46,9 @@ class IconsHelper extends \Prefab {
switch($attr['type']){
case 'status-selector':
$selected = Base::instance()->get('GET.status') ?: null;
return '<?php echo \IconsHelper::instance()->renderStatusSelector("'.$selected.'", "'.$path.'"); ?>';
// $selected = Base::instance()->get('GET.status') ?: null;
return '<?php echo \IconsHelper::instance()->renderStatusSelector(
Base::instance()->get("GET.status") ?: null, "'.$path.'"); ?>';
return self::renderStatusSelector($selected, $path);
default:
@ -87,7 +88,7 @@ class IconsHelper extends \Prefab {
foreach (self::$status_icons as $k => $icon) {
$active = ($current_status == $k);
$url = $path . ($active ? '' : '/?status=' . $k);
$class = 'button' . ($active ? ' is-primary' : '');
$class = 'button' . ($active ? ' is-inverted' : '');
$output .= '<p class="control">';
$output .= '<a href="' . $url . '" class="' . $class . '">';

View File

@ -63,6 +63,7 @@ class Ticket extends \DB\SQL\Mapper {
public function getTagsForTickets(array $tickets)
{
if(empty($tickets)) return [];
$tag_mapper = new Tag($this->db, 'ticket');
$tickets = $tag_mapper->getTagsFor($tickets);
@ -74,8 +75,34 @@ class Ticket extends \DB\SQL\Mapper {
$this->status_name = 'SELECT name FROM ticket_statuses WHERE tickets.status_id = ticket_statuses.id';
$this->priority_name = 'SELECT name FROM ticket_priorities WHERE tickets.priority_id = ticket_priorities.id';
$this->load(['id = ?', $id]);
$this->tags = (new Tag($this->db,'ticket'))->getTagsForID($id, 'ticket_id');
return $this->dry() ? null : $this;
if($this->dry()){
return null;
}
$tag_model = new Tag($this->db, 'ticket');
$this->tags = $tag_model->getTagsForID($this->id, 'ticket_id');
return $this;
}
public function setTags(array $tags_ids):void {
if($this->dry() || !$this->id){
// can't set tags for a ticket that hasn't been saved or loaded
return;
}
// remove existing tags - TODO: shouldn't this be in the tag model?
$this->db->exec('DELETE FROM ticket_tags WHERE ticket_id = ?', [$this->id]);
if(!empty($tags_ids)){
$sql_insert_tag = 'INSERT INTO ticket_tags (ticket_id, tag_id) VALUES (?,?)';
foreach($tags_ids as $tag_id){
if(filter_var($tag_id, FILTER_VALIDATE_INT)){
$this->db->exec($sql_insert_tag, [$this->id, (int)$tag_id]);
}
}
}
// refresh tags
$tag_model = new Tag($this->db, 'ticket');
$this->tags = $tag_model->getTagsForID($this->id, 'ticket_id');
}
public function createTicket(array $data): int
@ -93,11 +120,15 @@ class Ticket extends \DB\SQL\Mapper {
$this->updated_at = date('Y-m-d H:i:s');
$this->save();
$this->logCreate();
return (int)$this->id;
}
public function updateTicket(array $data): void
{
$this->logDiff($data);
if(isset($data['title'])){ $this->title = $data['title']; }
if(isset($data['description'])) { $this->description = $data['description']; }
if(isset($data['priority_id'])) { $this->priority_id = $data['priority_id']; }
@ -113,8 +144,6 @@ class Ticket extends \DB\SQL\Mapper {
$this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s');
$this->updated_at = date('Y-m-d H:i:s');
// printf('<pre>%s</pre>', print_r($this,1)); exit();
$this->save();
}
@ -244,5 +273,79 @@ class Ticket extends \DB\SQL\Mapper {
$user = $this->db->exec($sql, [$this->assigned_to]);
return $user[0];
}
/**
* Logs a change to the ticket history.
* @param int $user_id - the ID of the user making the change.
* @param string $field_changed - the name of the field that was changed.
* @param string|null $old_value - the old value
* @param string|null $new_value - the new value
*/
public function logHistory(int $user_id, string $field_changed,
$old_value = null, $new_value = null,
$changed_at = null): void
{
if($this->dry() || !$this->id) return;
$history = new \DB\SQL\Mapper($this->db, 'ticket_history');
$history->ticket_id = $this->id;
$history->user_id = $user_id;
$history->field_changed = $field_changed;
$history->old_value = $old_value === null ? null : (string)$old_value;
$history->new_value = $new_value === null ? null : (string)$new_value;
if($changed_at == null){
$history->changed_at = date('Y-m-d H:i:s');
} else {
$history->changed_at = $changed_at;
}
$history->save();
}
public function getHistory(): array {
if($this->dry() || !$this->id) return[];
$sql = 'SELECT th.*, u.display_name as user_display_name
FROM ticket_history th
JOIN users u ON th.user_id = u.id
WHERE th.ticket_id = ?
ORDER BY th.changed_at DESC';
return $this->db->exec($sql, [$this->id]);
}
/**
* called from create
*/
public function logCreate(){
$changed_at = date('Y-m-d H:i:s');
$user_making_change = \Base::instance()->get('SESSION.user.id');
$this->logHistory($user_making_change, 'ticket_created', null, $this->title, $changed_at);
if($this->status_id) $this->logHistory($user_making_change, 'status_id', null, $this->status_id, $changed_at);
if($this->priority_id) $this->logHistory($user_making_change, 'priority_id', null, $this->priority_id, $changed_at);
if($this->assigned_to) $this->logHistory($user_making_change, 'assigned_to', null, $this->assigned_to, $changed_at);
}
/**
* called from update
*/
public function logDiff($data){
$user_making_change = \Base::instance()->get('SESSION.user.id');
$changed_at = date('Y-m-d H:i:s');
$checks = ['title', 'description', 'priority_id', 'status_id', 'assigned_to', 'updated_by'];
// loop instead
foreach($checks as $check){
if(isset($data[$check]) && $this->$check != $data[$check]){
if($check == 'description'){
$old_hash = hash('sha256', $this->$check);
$new_hash = hash('sha256', $data[$check]);
$this->logHistory($user_making_change, $check, $old_hash, $new_hash, $changed_at);
} else {
$this->logHistory($user_making_change, $check, $this->$check, $data[$check], $changed_at);
}
}
}
}
}

View File

@ -54,15 +54,29 @@
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id"
options="{{@priorities}}" option_value="id" option_name="name" selected="0"></bulma>
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id" options="{{@priorities}}"
option_value="id" option_name="name" selected="0"></bulma>
<bulma type="FIELD_SELECT" label="Status:" name="status_id"
options="{{@statuses}}" option_value="id" option_name="name" selected="0"></bulma>
<bulma type="FIELD_SELECT" label="Status:" name="status_id" options="{{@statuses}}" option_value="id"
option_name="name" selected="0"></bulma>
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to"
options="{{@users}}" option_value="id" option_name="display_name" selected="0"></bulma>
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to" options="{{@users}}"
option_value="id" option_name="display_name" selected="0"></bulma>
<!-- field select multiple (TODO: move this to a custom bulma tag)-->
<div class="field">
<label class="label">Tags</label>
<div class="control">
<div class="select is-multiple" style="width:100%">
<select name="tags[]" multiple size="5" style="width:100%">
<repeat group="{{ @all_tags }}" value="{{@tag_option}}">
<option value="{{ @tag_option.id}}">{{ @tag_option.name }}</option>
</repeat>
</select>
</div>
<p class="help">Hold Ctrl to select multiple.</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,93 +2,116 @@
<!-- made to look more in line with view-->
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
{{ \CSRFHelper::field() | raw }}
<div class="is-flex">
<div class="is-flex-grow-1">
<bulma type="FIELD_INPUT" name="title" value="{{@ticket.title}}" class="mr-3"></bulma>
</div>
<div class="field is-grouped">
<div class="control">
<a class="button is-secondary" href="/ticket/{{ @ticket.id }}">Cancel</a>
<div class="is-flex">
<div class="is-flex-grow-1">
<bulma type="FIELD_INPUT" name="title" value="{{@ticket.title}}" class="mr-3"></bulma>
</div>
<div class="control">
<button class="button is-primary" type="submit">Save</button>
</div>
</div>
</div>
<hr>
<div class="block">
<div class="columns">
<div class="column is-two-thirds">
<div class="block">
<bulma type="FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}"></bulma>
<div class="field is-grouped">
<div class="control">
<a class="button is-secondary" href="/ticket/{{ @ticket.id }}">Cancel</a>
</div>
<div class="control">
<button class="button is-primary" type="submit">Save</button>
</div>
</div>
</div>
<div class="tabs">
<ul>
<li class="is-active">
<a data-tab="write"><span class="icon is-small">
<i class="fas fa-pen" aria-hidden="true"></i></span><span>Write</span>
</a>
</li>
<li>
<a data-tab="preview"><span class="icon is-small">
<i class="fas fa-magnifying-glass" aria-hidden="true"></i></span><span>Preview</span>
</a>
</li>
</ul>
</div>
<hr>
<div class="block">
<div id="tab-write" class="tab-content block">
<div class="columns">
<div class="column is-two-thirds">
<div class="block">
<bulma type="FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}">
</bulma>
</div>
<div class="tabs">
<ul>
<li class="is-active">
<a data-tab="write"><span class="icon is-small">
<i class="fas fa-pen" aria-hidden="true"></i></span><span>Write</span>
</a>
</li>
<li>
<a data-tab="preview"><span class="icon is-small">
<i class="fas fa-magnifying-glass"
aria-hidden="true"></i></span><span>Preview</span>
</a>
</li>
</ul>
</div>
<div id="tab-write" class="tab-content block">
<bulma type="FIELD_TEXTAREA" name="description" value="{{@ticket.description}}" rows="20"></bulma>
</div>
<div id="tab-preview" class="tab-content content">
</div>
<div id="tab-preview" class="tab-content content">
<div id="preview-output"></div>
</div>
</div>
</div>
<div class="column">
<div class="block">
<!-- priority and status -->
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id"
options="{{@priorities}}" option_value="id" option_name="name"
selected="{{@ticket.priority_id}}"></bulma>
<bulma type="FIELD_SELECT" label="Status:" name="status_id"
options="{{@statuses}}" option_value="id" option_name="name"
selected="{{@ticket.status_id}}"></bulma>
<div class="column">
<div class="block">
<!-- priority and status -->
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id" options="{{@priorities}}"
option_value="id" option_name="name" selected="{{@ticket.priority_id}}"></bulma>
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to"
options="{{@users}}" option_value="id" option_name="display_name"
selected="{{@ticket.assigned_to}}"></bulma>
<bulma type="FIELD_SELECT" label="Status:" name="status_id" options="{{@statuses}}"
option_value="id" option_name="name" selected="{{@ticket.status_id}}"></bulma>
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to" options="{{@users}}"
option_value="id" option_name="display_name" selected="{{@ticket.assigned_to}}"></bulma>
<!-- tags - need to implement a bulma-->
<div class="field">
<label class="label">Tags</label>
<div class="control">
<div class="select is-multiple" style="width:100%;">
<select name="tags[]" id="tags" multiple size="8" style="width:100%;">
<repeat group="{{ @all_tags }}" value="{{ @tag_option }}">
<option value="{{ @tag_option.id}}" {{ in_array(@tag_option.id,
@current_ticket_tag_ids) ? 'selected' : '' }}>
{{ @tag_option.name }}
</option>
</repeat>
</select>
</div>
<p class="help">Hold Ctrl to select multiple tags.</p>
</div>
</div>
<!-- additional data -->
<div class="block">
</div>
<!-- meta data -->
<div class="block">
<table class="table is-bordered is-fullwidth">
<thead>
<tr><th class="has-width-100">Property</th><th>Value</th></tr>
</thead>
<tbody>
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
<check if="{{ @key !== 'description'}}">
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
</check>
</repeat>
</tbody>
</table>
</div>
<!-- form to add child ticket relationships -->
<div class="box">
<h4 class="title is-4">Linked Tickets</h4>
<!-- parent -->
<?php
</div>
<!-- additional data -->
<div class="block">
</div>
<!-- meta data -->
<div class="block">
<table class="table is-bordered is-fullwidth">
<thead>
<tr>
<th class="has-width-100">Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
<check if="{{ @key !== 'description'}}">
<tr>
<td>{{@key}}</td>
<td>{{@value}}</td>
</tr>
</check>
</repeat>
</tbody>
</table>
</div>
<!-- form to add child ticket relationships -->
<div class="box">
<h4 class="title is-4">Linked Tickets</h4>
<!-- parent -->
<?php
/*
<check if="{{ @parent_tickets }}">
<div class="block">
@ -126,19 +149,19 @@
</div>
</form>
*/ ?>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<hr>
<div>
<exclude>
<include href="views/attachment/index.html"></include>
<include href="views/comments/view.html"></include>
<include href="views/attachment/index.html"></include>
<include href="views/comments/view.html"></include>
</exclude>
@ -147,4 +170,4 @@
<div class="block" id="comments"></div>
-->
</div>
</div>

View File

@ -22,24 +22,9 @@
</div>
<div class="block">
<icons type="status-selector" selected="{{ @GET.status ?: null }}" path="tickets"></icons>25
<icons type="status-selector" selected="{{ @GET.status ?: null }}" path="tickets"></icons>
</div>
<exclude>
<div class="block">
<div class="field has-addons">
<!-- TODO: move this into a template -->
<repeat group="{{ IconsHelper::$status_icons}}" key="{{ @k }}" value="{{ @icon }}">
<p class="control">
<a href="{{ @PATH }}/?status={{ @k }}" class="button">
<span class="icon is-small"><i class="fas fa-{{ @icon[0] }}"></i></span>
<span>{{ IconsHelper::$status_names[@k] }}</span>
</a>
</p>
</repeat>
</div>
</div>
</exclude>
<hr>
<div id="ticket_list">

View File

@ -101,6 +101,73 @@
</div>
<hr>
<div class="block" id="ticket-history">
<h4 class="title is-4">Ticket History</h4>
<check if="{{ !empty(@ticket_history) }}">
<div class="list is-hoverable">
<repeat group="{{ @ticket_history }}" value="{{ @entry }}">
<div class="list-item">
<div class="list-item-content">
<div class="list-item-title">
<small class="has-text-grey">
{{ date('Y-m-d H:i:s', strtotime(@entry.changed_at)) }} by {{ @entry.user_display_name }}
</small>
</div>
<div class="list-item-description">
<!-- creation -->
<check if="{{ @entry.field_changed == 'ticket_created' }}">
Ticket created: {{ @entry.new_value}}
</check>
<!-- status -->
<check if="{{ @entry.field_changed == 'status_id' }}">
Status changed
<check if="{{ @entry.old_value !== null }}">
from <strong>{{ @map['status'][@entry.old_value] ?? 'Unknown' }}</strong>
</check>
to <strong>{{ @map['status'][@entry.new_value] ?? 'Unknown' }}</strong>
</check>
<!-- priority -->
<check if="{{ @entry.field_changed == 'priority_id' }}">
Priority changed
<check if="{{ @entry.old_value !== null }}">
from <strong>{{ @map['priorities'][@entry.old_value] ?? 'Unknown'}}</strong>
</check>
to <strong>{{ @map['priorities'][@entry.new_value] ?? 'Unknown' }}</strong>
</check>
<!-- assignment -->
<check if="{{ @entry.field_changed == 'assigned_to' }}">
Assignment changed
<check if="{{ @entry.old_value !== null && @entry.old_value != 0 }}">
from <strong>{{ @map['users'][@entry.old_value] ?? 'Unassigned' }}</strong>
</check>
<check if="{{ @entry.old_value === null || @entry.old_value == 0}}">
from <strong>Unassigned</strong>
</check>
to
<check if="{{ @entry.new_value !== null && @entry.new_value != 0}}">
<strong>{{ @map['users'][@entry.new_value] ?? 'Unassigned' }}</strong>
</check>
<check if="{{ @entry.new_value === null || @entry.new_value == 0}}">
<strong>Unassigned</strong>
</check>
</check>
<check if="{{ @entry.field_changed == 'title'}}">
Title changed from "{{ @entry.old_value}}" to "{{ @entry.new_value}}".
</check>
<check if="{{ @entry.field_changed == 'description'}}">
Description updated old sha256({{@entry.old_value}}).
</check>
</div>
</div>
</div>
</repeat>
</div>
<false>
<p>No history entries for this ticket.</p>
</false>
</check>
<!--
<div class="block" id="attachments"></div>
<div class="block" id="comments"></div>