Compare commits

..

2 Commits

13 changed files with 278 additions and 19 deletions

View File

@ -1,12 +1,41 @@
$(document).ready -> $(document).ready ->
console.log('Hello, world!') $('.ticket-queue').on('click', (e) ->
handleQueueClick(e)
$('.ticket-severity').on('click', handleAttributeClick('severity')) )
$('.ticket-status').on('click', handleAttributeClick('status')) $('.ticket-severity').on('click', (e) ->
handleAttributeClick(e, 'severity')
)
$('.ticket-status').on('click', (e) ->
handleAttributeClick(e, 'status')
)
validOptions = validOptions =
'severity' = ['low', 'medium', 'high'], 'severity': ['low', 'medium', 'high'],
'status' = ['open', 'closed', 'parked'] 'status': ['open', 'closed', 'parked']
handleQueueClick = (e, fail = false) ->
newQueueId = prompt('Set queue ID:', $('.ticket-queue').data('id'))
if (newQueueId != null) and (newQueueId != '')
if (true)
console.log('Setting queue ID to ' + newQueueId)
editLink = $('#ticketEditLink').attr('href') + '/queue_id'
console.log('Sending data to ' + editLink)
$.ajax({
type: "POST",
url: editLink,
data:
'queue_id': newQueueId,
dataType: 'json',
success: (result) ->
$('.ticket-queue').data('id', newQueueId)
$('.ticket-queue > span').text(result.queue_name)
updateTicketModified(result.updated_at)
console.log('Ticket updated successfully.')
})
else
console.log('Invalid queue ID entered')
handleQueueClick(e, 'Invalid queue ID entered; you must enter a number.')
handleAttributeClick = (e, attr, fail = false) -> handleAttributeClick = (e, attr, fail = false) ->
newValue = prompt('Set ticket ' + attr + ':', $('.ticket-' + attr + ' > span').text()) newValue = prompt('Set ticket ' + attr + ':', $('.ticket-' + attr + ' > span').text())
@ -16,12 +45,13 @@ handleAttributeClick = (e, attr, fail = false) ->
if (newValue in validOptions[attr]) if (newValue in validOptions[attr])
console.log('Setting ' + attr + ' to ' + newValue) console.log('Setting ' + attr + ' to ' + newValue)
editLink = $('#ticketEditLink').attr('href') + '/' + attr editLink = $('#ticketEditLink').attr('href') + '/' + attr
postData = {}
postData[attr] = newValue
console.log('Sending data to ' + editLink) console.log('Sending data to ' + editLink)
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: editLink, url: editLink,
data: data: postData,
attr: newValue,
dataType: 'json', dataType: 'json',
success: (result) -> success: (result) ->
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1) newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1)

View File

@ -81,12 +81,16 @@ input[type="submit"].button-primary
height: 250px height: 250px
min-height: 100px min-height: 100px
#queue-header,
#ticket-header #ticket-header
margin-bottom: 15px margin-bottom: 15px
.queue-title,
.ticket-title .ticket-title
margin-bottom: 5px margin-bottom: 5px
.queue-created,
.queue-updated,
.ticket-created, .ticket-created,
.ticket-updated .ticket-updated
margin-bottom: 3px margin-bottom: 3px
@ -94,6 +98,7 @@ input[type="submit"].button-primary
font-size: 1.5rem font-size: 1.5rem
font-style: italic font-style: italic
#queue-description,
#ticket-body #ticket-body
p:last-child p:last-child
margin-bottom: 5px margin-bottom: 5px
@ -141,6 +146,7 @@ input[type="submit"].button-primary
margin-right: 5px margin-right: 5px
font-size: 2rem font-size: 2rem
.ticket-queue,
.ticket-severity, .ticket-severity,
.ticket-status .ticket-status
transition: all 230ms ease-in-out transition: all 230ms ease-in-out

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddQueueTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
// Create table for Queue objects
$table = $this->table('queues');
$table->addColumn('title', 'string', ['null' => false])
->addColumn('description', 'text', ['null' => false])
->addTimestamps()
->addIndex(['title'])
->create();
// Update tickets table to have a Queue ID field
$tickets = $this->table('tickets')
->addColumn('queue_id', 'integer', ['null' => false])
->addForeignKey('queue_id', 'queues', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->update();
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace BitGoblin\Goliath\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteContext;
use Slim\Views\Twig;
use BitGoblin\Goliath\Models\Queue;
class QueueController extends Controller {
public function getView(Request $request, Response $response, array $args): Response {
$queue = Queue::where('id', $args['queue_id'])->first();
$view = Twig::fromRequest($request);
return $view->render($response, 'queue/view.twig', [
'queue' => $queue,
]);
}
public function getCreate(Request $request, Response $response): Response {
$view = Twig::fromRequest($request);
return $view->render($response, 'queue/create.twig');
}
public function postCreate(Request $request, Response $response): Response {
$params = (array)$request->getParsedBody();
$queue = new Queue;
$queue->title = $params['queue_title'];
$queue->description = $params['queue_description'];
$queue->save();
// redirect the user back to the home page
$routeContext = RouteContext::fromRequest($request);
$routeParser = $routeContext->getRouteParser();
return $response
->withHeader('Location', $routeParser->urlFor('queue.view', ['queue_id' => $queue->id]))
->withStatus(302);
}
}

View File

@ -6,6 +6,7 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteContext; use Slim\Routing\RouteContext;
use Slim\Views\Twig; use Slim\Views\Twig;
use BitGoblin\Goliath\Models\Queue;
use BitGoblin\Goliath\Models\Ticket; use BitGoblin\Goliath\Models\Ticket;
class TicketController extends Controller { class TicketController extends Controller {
@ -20,8 +21,12 @@ class TicketController extends Controller {
} }
public function getCreate(Request $request, Response $response): Response { public function getCreate(Request $request, Response $response): Response {
$queues = Queue::all();
$view = Twig::fromRequest($request); $view = Twig::fromRequest($request);
return $view->render($response, 'ticket/create.twig'); return $view->render($response, 'ticket/create.twig', [
'queues' => $queues,
]);
} }
public function postCreate(Request $request, Response $response): Response { public function postCreate(Request $request, Response $response): Response {
@ -31,6 +36,7 @@ class TicketController extends Controller {
$ticket->title = $params['ticket_title']; $ticket->title = $params['ticket_title'];
$ticket->body = $params['ticket_body']; $ticket->body = $params['ticket_body'];
$ticket->severity = $params['ticket_severity']; $ticket->severity = $params['ticket_severity'];
$ticket->queue_id = $params['ticket_queue'];
$ticket->due_at = $params['ticket_due']; $ticket->due_at = $params['ticket_due'];
$ticket->save(); $ticket->save();
@ -81,11 +87,17 @@ class TicketController extends Controller {
// save updated ticket // save updated ticket
$ticket->save(); $ticket->save();
// return a response // build JSON response
$response->getBody()->write(json_encode([ $jsonResponse = [
'result' => 'success', 'result' => 'success',
'updated_at' => $ticket->formatUpdatedAt(), 'updated_at' => $ticket->formatUpdatedAt(),
])); ];
if ($attr == 'queue_id') {
$jsonResponse['queue_name'] = $ticket->queue->title;
}
// return a response
$response->getBody()->write(json_encode($jsonResponse));
return $response; return $response;
} }

36
src/Models/Queue.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace BitGoblin\Goliath\Models;
use Illuminate\Database\Eloquent\Model;
use League\CommonMark\CommonMarkConverter;
class Queue extends Model {
protected $fillable = [
'title',
'description',
];
public function tickets() {
return $this->hasMany(Ticket::class);
}
public function render(): string {
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convert($this->description);
}
public function formatCreatedAt(): string {
return date_format(date_create($this->created_at), "F jS\\, Y \\a\\t g:i:s a");
}
public function formatUpdatedAt(): string {
return date_format(date_create($this->updated_at), "F jS\\, Y \\a\\t g:i:s a");
}
}

View File

@ -14,6 +14,14 @@ class Ticket extends Model {
'due_at', 'due_at',
]; ];
public function queue() {
return $this->belongsTo(Queue::class);
}
public function comments() {
return $this->hasMany(Comment::class);
}
public function render(): string { public function render(): string {
$converter = new CommonMarkConverter([ $converter = new CommonMarkConverter([
'html_input' => 'strip', 'html_input' => 'strip',
@ -23,10 +31,6 @@ class Ticket extends Model {
return $converter->convert($this->body); return $converter->convert($this->body);
} }
public function comments() {
return $this->hasMany(Comment::class);
}
public function formatCreatedAt(): string { public function formatCreatedAt(): string {
return date_format(date_create($this->created_at), "F jS\\, Y \\a\\t g:i:s a"); return date_format(date_create($this->created_at), "F jS\\, Y \\a\\t g:i:s a");
} }

View File

@ -7,6 +7,13 @@ use Slim\Views\Twig;
// index GET route - this page should welcome the user and direct them to the available actions // index GET route - this page should welcome the user and direct them to the available actions
$app->get('/', '\\BitGoblin\\Goliath\\Controllers\\HomeController:getIndex')->setName('index'); $app->get('/', '\\BitGoblin\\Goliath\\Controllers\\HomeController:getIndex')->setName('index');
// /queue/create routes - allows a user to fill out a form to create a new queue
$app->get('/queue/create', '\\BitGoblin\\Goliath\\Controllers\\QueueController:getCreate')->setName('queue.create');
$app->post('/queue/create', '\\BitGoblin\\Goliath\\Controllers\\QueueController:postCreate');
// /queue/id route - displays queue info to user
$app->get('/queue/{queue_id}', '\\BitGoblin\\Goliath\\Controllers\QueueController:getView')->setName('queue.view');
// /ticket/create routes - allows a user to fill out a form to create a ticket // /ticket/create routes - allows a user to fill out a form to create a ticket
$app->get('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getCreate')->setName('ticket.create'); $app->get('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getCreate')->setName('ticket.create');
$app->post('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postCreate'); $app->post('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postCreate');

View File

@ -16,7 +16,8 @@
<ul class="nav-menu"> <ul class="nav-menu">
<li>Goliath</li> <li>Goliath</li>
<li class="nav-link"><a href="{{ url_for('index') }}">Home</a></li> <li class="nav-link"><a href="{{ url_for('index') }}">Home</a></li>
<li class="nav-link"><a href="{{ url_for('ticket.create') }}">Create</a></li> <li class="nav-link"><a href="{{ url_for('queue.create') }}">New Queue</a></li>
<li class="nav-link"><a href="{{ url_for('ticket.create') }}">New Ticket</a></li>
<li class="nav-link"><a href="/search">Search</a></li> <li class="nav-link"><a href="/search">Search</a></li>
</ul> </ul>
</div> </div>

29
views/queue/create.twig Normal file
View File

@ -0,0 +1,29 @@
{% extends 'layout.twig' %}
{% block title %}Create New Queue{% endblock %}
{% block content %}
<div class="row">
<div class="columns twelve">
<h1>Create new queue</h1>
</div>
</div>
<div class="row">
<div class="columns twelve">
<form id="queue-form" action="/queue/create" method="POST" class="u-full-width">
<div class="row">
<div class="twelve columns">
<label for="queue_title">Title</label>
<input id="queue_title" class="u-full-width" type="text" placeholder="My new queue" name="queue_title">
</div>
</div>
<label for="queue_description">Description</label>
<textarea id="queue_description" class="u-full-width" placeholder="Explain what this queue is about..." name="queue_description"></textarea>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}

44
views/queue/view.twig Normal file
View File

@ -0,0 +1,44 @@
{% extends 'layout.twig' %}
{% block title %}Ticket Queue: {{ queue.title }}{% endblock %}
{% block content %}
<!-- queue content -->
<div class="row">
<div class="twelve columns">
<div id="queue-header" class="row">
<div class="columns twelve">
<h1 class="queue-title">{{ queue.title }}</h1>
<h4 class="queue-created">Created at: <span>{{ queue.formatCreatedAt() }}</span></h4>
<h4 class="queue-updated">Last updated at: <span>{{ queue.formatUpdatedAt() }}</span></h4>
</div>
</div>
<div id="queue-description" class="row">
<div class="columns twelve">
{{ queue.render() | raw }}
</div>
</div>
</div>
</div>
<hr>
<!-- queue tickets -->
<div class="row">
<div class="twelve columns">
<ul id="queue-list" class="row">
<h3>Tickets in this queue:</h3>
{% if queue.tickets | length > 0 %}
{% for ticket in queue.tickets %}
<li>
<a href="{{ url_for('ticket.view', { ticket_id: ticket.id }) }}">{{ ticket.title }}</a>
</li>
{% endfor %}
{% else %}
<p>There are no tickets in this queue.</p>
{% endif %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -17,11 +17,19 @@
<label for="ticket_title">Title</label> <label for="ticket_title">Title</label>
<input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title"> <input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title">
</div> </div>
<div class="columns three"> <div class="two columns">
<label for="ticket_queue">Queue</label>
<select name="ticket_queue" id="ticket_queue" class="u-full-width">
{% for q in queues %}
<option value="{{ q.id }}">{{ q.title }}</option>
{% endfor %}
</select>
</div>
<div class="two columns">
<label for="ticket_due">Due at</label> <label for="ticket_due">Due at</label>
<input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due"> <input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due">
</div> </div>
<div class="three columns"> <div class="two columns">
<label for="ticket_severity">Severity level</label> <label for="ticket_severity">Severity level</label>
<select id="ticket_severity" class="u-full-width" name="ticket_severity"> <select id="ticket_severity" class="u-full-width" name="ticket_severity">
<option value="low">Low</option> <option value="low">Low</option>

View File

@ -29,6 +29,9 @@
<li><a id="ticketEditLink" href="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-pen-to-square"></i>Edit</a></li><li><a href="{{ url_for('ticket.delete', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-trash"></i>Delete</a></li> <li><a id="ticketEditLink" href="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-pen-to-square"></i>Edit</a></li><li><a href="{{ url_for('ticket.delete', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-trash"></i>Delete</a></li>
</ul> </ul>
</li> </li>
<li class="ticket-queue" data-id="{{ ticket.queue.id }}">
Queue: <span>{{ ticket.queue.title }}</span>
</li>
<li class="ticket-severity"> <li class="ticket-severity">
Severity: <span>{{ ticket.severity | capitalize }}</span> Severity: <span>{{ ticket.severity | capitalize }}</span>
</li> </li>