Compare commits
19 Commits
5fb463598a
...
main
Author | SHA1 | Date | |
---|---|---|---|
acbe69e9c7 | |||
80a12a86ef | |||
15ee9b78a3 | |||
ab2c87ce3a | |||
77c8605b49 | |||
5d5265ef38 | |||
f3ac8a6965 | |||
5683c0bc8b | |||
4371fd0b2f | |||
3ac8671742 | |||
b347988937 | |||
55048bda33 | |||
17c2e36bb5 | |||
c6d4c3df10 | |||
95bf9250e7 | |||
830a950bf4 | |||
41aca6215e | |||
4bf5e73179 | |||
f4cf324280 |
13
.gitignore
vendored
13
.gitignore
vendored
@ -1,5 +1,18 @@
|
||||
# Composer dependencies
|
||||
vendor/
|
||||
|
||||
# Local data
|
||||
data/
|
||||
|
||||
# NPM dependencies (building CSS and JS)
|
||||
node_modules/
|
||||
|
||||
# Compiled CSS and JS
|
||||
public/styles/
|
||||
public/js/
|
||||
|
||||
# SASS compilation cache
|
||||
.sass-cache/
|
||||
|
||||
# PHP CodeSniffer cache
|
||||
.phpcs-cache
|
||||
|
65
Gruntfile.js
Normal file
65
Gruntfile.js
Normal file
@ -0,0 +1,65 @@
|
||||
module.exports = function(grunt) {
|
||||
|
||||
var pkg = grunt.file.readJSON('package.json')
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
sass: {
|
||||
dist: {
|
||||
options: {
|
||||
style: 'compressed'
|
||||
},
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'assets/sass',
|
||||
src: ['*.sass'],
|
||||
dest: 'public/styles',
|
||||
ext: '.css'
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
coffee: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
style: 'compressed'
|
||||
},
|
||||
files: {
|
||||
expand: true,
|
||||
flatten: true,
|
||||
cwd: 'assets/coffee',
|
||||
src: ['*.coffee'],
|
||||
dest: 'public/js',
|
||||
ext: '.js'
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
css: {
|
||||
files: ['assets/sass/*.sass'],
|
||||
tasks: ['sass'],
|
||||
options: {
|
||||
atBegin: true,
|
||||
spawn: false
|
||||
}
|
||||
},
|
||||
js: {
|
||||
files: ['assets/coffee/*.coffee'],
|
||||
tasks: ['coffee'],
|
||||
options: {
|
||||
atBegin: true,
|
||||
spawn: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load task plugins
|
||||
grunt.loadNpmTasks('grunt-contrib-sass');
|
||||
grunt.loadNpmTasks('grunt-contrib-coffee');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
|
||||
// Default task(s).
|
||||
grunt.registerTask('default', ['sass', 'coffee']);
|
||||
|
||||
};
|
68
assets/coffee/slepe.coffee
Normal file
68
assets/coffee/slepe.coffee
Normal file
@ -0,0 +1,68 @@
|
||||
$(document).ready ->
|
||||
$('.ticket-queue').on('click', (e) ->
|
||||
handleQueueClick(e)
|
||||
)
|
||||
$('.ticket-severity').on('click', (e) ->
|
||||
handleAttributeClick(e, 'severity')
|
||||
)
|
||||
$('.ticket-status').on('click', (e) ->
|
||||
handleAttributeClick(e, 'status')
|
||||
)
|
||||
|
||||
validOptions =
|
||||
'severity': ['low', 'medium', 'high'],
|
||||
'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) ->
|
||||
newValue = prompt('Set ticket ' + attr + ':', $('.ticket-' + attr + ' > span').text())
|
||||
newValue = newValue.toLowerCase()
|
||||
|
||||
if (newValue != null) and (newValue != '')
|
||||
if (newValue in validOptions[attr])
|
||||
console.log('Setting ' + attr + ' to ' + newValue)
|
||||
editLink = $('#ticketEditLink').attr('href') + '/' + attr
|
||||
postData = {}
|
||||
postData[attr] = newValue
|
||||
console.log('Sending data to ' + editLink)
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: editLink,
|
||||
data: postData,
|
||||
dataType: 'json',
|
||||
success: (result) ->
|
||||
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1)
|
||||
$('.ticket-' + attr + ' > span').text(newValue)
|
||||
updateTicketModified(result.updated_at)
|
||||
console.log('Ticket updated successfully.')
|
||||
})
|
||||
else
|
||||
console.log('Invalid ' + attr + ' entered')
|
||||
handleAttributeClick(e, attr, 'Invalid ' + attr + '; valid values are ' + validOptions.toString() + '.')
|
||||
|
||||
updateTicketModified = (date) ->
|
||||
$('.ticket-updated > span').text(date)
|
||||
console.log('Ticket update time is ' + date)
|
169
assets/sass/darkmeyer.sass
Normal file
169
assets/sass/darkmeyer.sass
Normal file
@ -0,0 +1,169 @@
|
||||
$primary-color: #009688
|
||||
$primary-color-highlight: lighten($primary-color, 10%)
|
||||
$accent-color: #795548
|
||||
$accent-color-highlight: lighten($accent-color, 10%)
|
||||
|
||||
$box-shadow-1: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)
|
||||
$box-shadow-2: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)
|
||||
|
||||
$nav-height: 75px
|
||||
|
||||
body
|
||||
padding-top: $nav-height
|
||||
padding-bottom: 50px
|
||||
background: lightgrey
|
||||
font-size: 16px
|
||||
|
||||
a
|
||||
color: $accent-color
|
||||
transition: all 230ms ease-in-out
|
||||
&:hover
|
||||
color: $accent-color-highlight
|
||||
|
||||
.button.button-primary,
|
||||
button.button-primary,
|
||||
input[type="button"].button-primary,
|
||||
input[type="reset"].button-primary,
|
||||
input[type="submit"].button-primary
|
||||
background-color: $primary-color
|
||||
border-color: $primary-color
|
||||
font-size: 1.5rem
|
||||
transition: all 230ms ease-in-out
|
||||
&:hover
|
||||
background-color: $primary-color-highlight
|
||||
border-color: $primary-color-highlight
|
||||
|
||||
.container
|
||||
max-width: 1100px
|
||||
padding: 20px 30px
|
||||
|
||||
.card
|
||||
background: white
|
||||
box-shadow: $box-shadow-2
|
||||
|
||||
#main-nav
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: $nav-height
|
||||
background: $primary-color
|
||||
color: white
|
||||
font-size: 3rem
|
||||
font-weight: bold
|
||||
box-shadow: $box-shadow-1
|
||||
z-index: 100
|
||||
|
||||
.nav-left
|
||||
float: left
|
||||
|
||||
.nav-menu
|
||||
list-style: none
|
||||
padding-left: 15px
|
||||
|
||||
li
|
||||
display: inline-block
|
||||
margin-top: 12px
|
||||
margin-left: 15px
|
||||
|
||||
.nav-link a
|
||||
color: white
|
||||
&:hover
|
||||
color: #eee
|
||||
|
||||
#main-wrapper
|
||||
margin-top: 25px
|
||||
|
||||
#ticket-form,
|
||||
#comment-form
|
||||
textarea
|
||||
max-width: 100%
|
||||
height: 250px
|
||||
min-height: 100px
|
||||
|
||||
#queue-header,
|
||||
#ticket-header
|
||||
margin-bottom: 15px
|
||||
|
||||
.queue-title,
|
||||
.ticket-title
|
||||
margin-bottom: 5px
|
||||
|
||||
.queue-created,
|
||||
.queue-updated,
|
||||
.ticket-created,
|
||||
.ticket-updated
|
||||
margin-bottom: 3px
|
||||
color: #666
|
||||
font-size: 1.5rem
|
||||
font-style: italic
|
||||
|
||||
#queue-description,
|
||||
#ticket-body
|
||||
p:last-child
|
||||
margin-bottom: 5px
|
||||
|
||||
.ticket-attributes
|
||||
list-style: none
|
||||
margin: 0
|
||||
border: 1px solid #bbb
|
||||
border-bottom: none
|
||||
box-shadow: $box-shadow-1
|
||||
font-size: 1.75rem
|
||||
|
||||
> li
|
||||
margin: 0
|
||||
padding: 10px 12px
|
||||
border-bottom: 1px solid #999
|
||||
|
||||
.ticket-actions
|
||||
padding: 0
|
||||
|
||||
ul
|
||||
list-style: none
|
||||
margin: 0
|
||||
|
||||
li
|
||||
display: inline
|
||||
margin: 0
|
||||
padding: 0
|
||||
&:not(:first-child)
|
||||
a
|
||||
border-left: 1px solid #999
|
||||
|
||||
a
|
||||
position: relative
|
||||
display: inline-block
|
||||
box-sizing: border-box
|
||||
width: 50%
|
||||
height: 100%
|
||||
padding: 10px 12px
|
||||
text-align: center
|
||||
&:hover
|
||||
background: rgba(0, 0, 0, .1)
|
||||
|
||||
i
|
||||
margin-right: 5px
|
||||
font-size: 2rem
|
||||
|
||||
.ticket-queue,
|
||||
.ticket-severity,
|
||||
.ticket-status
|
||||
transition: all 230ms ease-in-out
|
||||
&:hover
|
||||
background: rgba(0, 0, 0, .1)
|
||||
cursor: pointer
|
||||
|
||||
#comment-form
|
||||
textarea
|
||||
height: 150px
|
||||
|
||||
.comments-list
|
||||
list-style: none
|
||||
margin: 0
|
||||
|
||||
.comment
|
||||
&:not(:last-child)
|
||||
border-bottom: 1px solid #666
|
||||
p:last-child
|
||||
margin-bottom: 5px
|
@ -20,6 +20,9 @@
|
||||
"slim/psr7": "^1.6",
|
||||
"php-di/php-di": "^6.4",
|
||||
"slim/twig-view": "^3.3",
|
||||
"hassankhan/config": "^3.0"
|
||||
"hassankhan/config": "^3.0",
|
||||
"illuminate/database": "^9.40",
|
||||
"robmorgan/phinx": "^0.13.1",
|
||||
"league/commonmark": "^2.3"
|
||||
}
|
||||
}
|
||||
|
2307
composer.lock
generated
2307
composer.lock
generated
File diff suppressed because it is too large
Load Diff
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
30
db/migrations/20221120233343_add_tickets_table.php
Normal file
30
db/migrations/20221120233343_add_tickets_table.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddTicketsTable 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()
|
||||
{
|
||||
$table = $this->table('tickets');
|
||||
$table->addColumn('title', 'string', ['null' => false])
|
||||
->addColumn('body', 'text', ['null' => false])
|
||||
->addColumn('severity', 'string', ['default' => 'low'])
|
||||
->addColumn('due_at', 'datetime')
|
||||
->addTimestamps()
|
||||
->addIndex(['title', 'body', 'due_at', 'severity'])
|
||||
->create();
|
||||
}
|
||||
}
|
25
db/migrations/20221121043856_add_status_to_tickets_table.php
Normal file
25
db/migrations/20221121043856_add_status_to_tickets_table.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddStatusToTicketsTable 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()
|
||||
{
|
||||
$table = $this->table('tickets');
|
||||
$table->addColumn('status', 'string', ['null' => false, 'default' => 'open'])
|
||||
->update();
|
||||
}
|
||||
}
|
29
db/migrations/20221122032232_add_comments_table.php
Normal file
29
db/migrations/20221122032232_add_comments_table.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddCommentsTable 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()
|
||||
{
|
||||
$table = $this->table('comments');
|
||||
$table->addColumn('body', 'text', ['null' => false])
|
||||
->addColumn('ticket_id', 'integer', ['null' => false])
|
||||
->addTimestamps()
|
||||
->addForeignKey('ticket_id', 'tickets', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
|
||||
->addIndex(['body'])
|
||||
->create();
|
||||
}
|
||||
}
|
35
db/migrations/20221204223428_add_queue_table.php
Normal file
35
db/migrations/20221204223428_add_queue_table.php
Normal 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();
|
||||
}
|
||||
}
|
2892
package-lock.json
generated
Normal file
2892
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "goliath",
|
||||
"version": "1.0.0",
|
||||
"description": "Open source internal ticketing software",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"grunt": "grunt",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.metaunix.net/BitGoblin/goliath"
|
||||
},
|
||||
"keywords": [
|
||||
"ticketing"
|
||||
],
|
||||
"author": "Gregory Ballantine <gballantine@bitgoblin.tech>",
|
||||
"license": "BSD-2-Clause",
|
||||
"devDependencies": {
|
||||
"grunt": "^1.5.3",
|
||||
"grunt-contrib-coffee": "^2.1.0",
|
||||
"grunt-contrib-sass": "^2.0.0",
|
||||
"grunt-contrib-watch": "^1.1.0"
|
||||
}
|
||||
}
|
37
phinx.php
Normal file
37
phinx.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return
|
||||
[
|
||||
'paths' => [
|
||||
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
|
||||
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds'
|
||||
],
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_environment' => 'development',
|
||||
'production' => [
|
||||
'adapter' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'name' => 'production_db',
|
||||
'user' => 'root',
|
||||
'pass' => '',
|
||||
'port' => '3306',
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
'development' => [
|
||||
'adapter' => 'sqlite',
|
||||
'name' => './data/goliath',
|
||||
'suffix' => '.db'
|
||||
],
|
||||
'testing' => [
|
||||
'adapter' => 'mysql',
|
||||
'host' => 'localhost',
|
||||
'name' => 'testing_db',
|
||||
'user' => 'root',
|
||||
'pass' => '',
|
||||
'port' => '3306',
|
||||
'charset' => 'utf8',
|
||||
]
|
||||
],
|
||||
'version_order' => 'creation'
|
||||
];
|
29
src/Controllers/CommentController.php
Normal file
29
src/Controllers/CommentController.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\Goliath\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Routing\RouteContext;
|
||||
use BitGoblin\Goliath\Models\Comment;
|
||||
|
||||
class CommentController extends Controller {
|
||||
|
||||
public function postAdd(Request $request, Response $response): Response {
|
||||
$params = (array)$request->getParsedBody();
|
||||
|
||||
$comment = new Comment;
|
||||
$comment->body = $params['comment_body'];
|
||||
$comment->ticket_id = $params['ticket_id'];
|
||||
|
||||
$comment->save();
|
||||
|
||||
// redirect the user back to the home page
|
||||
$routeContext = RouteContext::fromRequest($request);
|
||||
$routeParser = $routeContext->getRouteParser();
|
||||
return $response
|
||||
->withHeader('Location', $routeParser->urlFor('ticket.view', ['ticket_id' => $comment->ticket->id]))
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
}
|
@ -5,12 +5,18 @@ namespace BitGoblin\Goliath\Controllers;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Views\Twig;
|
||||
use BitGoblin\Goliath\Models\Ticket;
|
||||
|
||||
class HomeController extends Controller {
|
||||
|
||||
public function getIndex(Request $request, Response $response): Response {
|
||||
// find tickets from the database
|
||||
$tickets = Ticket::all();
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'index.twig');
|
||||
return $view->render($response, 'index.twig', [
|
||||
'tickets' => $tickets,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
44
src/Controllers/QueueController.php
Normal file
44
src/Controllers/QueueController.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
114
src/Controllers/TicketController.php
Normal file
114
src/Controllers/TicketController.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?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;
|
||||
use BitGoblin\Goliath\Models\Ticket;
|
||||
|
||||
class TicketController extends Controller {
|
||||
|
||||
public function getView(Request $request, Response $response, array $args): Response {
|
||||
$ticket = Ticket::where('id', $args['ticket_id'])->first();
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'ticket/view.twig', [
|
||||
'ticket' => $ticket,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCreate(Request $request, Response $response): Response {
|
||||
$queues = Queue::all();
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'ticket/create.twig', [
|
||||
'queues' => $queues,
|
||||
]);
|
||||
}
|
||||
|
||||
public function postCreate(Request $request, Response $response): Response {
|
||||
$params = (array)$request->getParsedBody();
|
||||
|
||||
$ticket = new Ticket;
|
||||
$ticket->title = $params['ticket_title'];
|
||||
$ticket->body = $params['ticket_body'];
|
||||
$ticket->severity = $params['ticket_severity'];
|
||||
$ticket->queue_id = $params['ticket_queue'];
|
||||
$ticket->due_at = $params['ticket_due'];
|
||||
|
||||
$ticket->save();
|
||||
|
||||
// redirect the user back to the home page
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function getEdit(Request $request, Response $response, array $args): Response {
|
||||
$ticket = Ticket::where('id', $args['ticket_id'])->get();
|
||||
|
||||
$view = Twig::fromRequest($request);
|
||||
return $view->render($response, 'ticket/edit.twig', [
|
||||
'ticket' => $ticket[0],
|
||||
]);
|
||||
}
|
||||
|
||||
public function postEdit(Request $request, Response $response, array $args): Response {
|
||||
$ticket = Ticket::where('id', $args['ticket_id'])->first();
|
||||
|
||||
$params = (array)$request->getParsedBody();
|
||||
$ticket->title = $params['ticket_title'];
|
||||
$ticket->body = $params['ticket_body'];
|
||||
$ticket->severity = $params['ticket_severity'];
|
||||
$ticket->due_at = $params['ticket_due'];
|
||||
|
||||
$ticket->save();
|
||||
|
||||
// redirect the user back to the home page
|
||||
$routeContext = RouteContext::fromRequest($request);
|
||||
$routeParser = $routeContext->getRouteParser();
|
||||
return $response
|
||||
->withHeader('Location', $routeParser->urlFor('ticket.view', ['ticket_id' => $ticket->id]))
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function postEditAttr(Request $request, Response $response, array $args): Response {
|
||||
// grab ticket from database
|
||||
$ticket = Ticket::where('id', $args['ticket_id'])->first();
|
||||
|
||||
// edit the specified attribute
|
||||
$params = (array)$request->getParsedBody();
|
||||
$attr = $args['attr'];
|
||||
$ticket->$attr = $params[$attr];
|
||||
|
||||
// save updated ticket
|
||||
$ticket->save();
|
||||
|
||||
// build JSON response
|
||||
$jsonResponse = [
|
||||
'result' => 'success',
|
||||
'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;
|
||||
}
|
||||
|
||||
public function getDelete(Request $request, Response $response, array $args): Response {
|
||||
$ticket = Ticket::where('id', $args['ticket_id'])->first();
|
||||
|
||||
$ticket->delete();
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
}
|
28
src/Models/Comment.php
Normal file
28
src/Models/Comment.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\Goliath\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class Comment extends Model {
|
||||
|
||||
protected $fillable = [
|
||||
'body',
|
||||
'ticket_id',
|
||||
];
|
||||
|
||||
public function render(): string {
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($this->body);
|
||||
}
|
||||
|
||||
public function ticket() {
|
||||
return $this->belongsTo(Ticket::class);
|
||||
}
|
||||
|
||||
}
|
36
src/Models/Queue.php
Normal file
36
src/Models/Queue.php
Normal 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");
|
||||
}
|
||||
|
||||
}
|
42
src/Models/Ticket.php
Normal file
42
src/Models/Ticket.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace BitGoblin\Goliath\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
|
||||
class Ticket extends Model {
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'body',
|
||||
'severity',
|
||||
'due_at',
|
||||
];
|
||||
|
||||
public function queue() {
|
||||
return $this->belongsTo(Queue::class);
|
||||
}
|
||||
|
||||
public function comments() {
|
||||
return $this->hasMany(Comment::class);
|
||||
}
|
||||
|
||||
public function render(): string {
|
||||
$converter = new CommonMarkConverter([
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
]);
|
||||
|
||||
return $converter->convert($this->body);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
@ -18,10 +18,16 @@ $container->set('config', function () use ($config) {
|
||||
return $config;
|
||||
});
|
||||
|
||||
// Load database configuration
|
||||
require_once __DIR__ . '/database.php';
|
||||
|
||||
// Set container to create App with on AppFactory
|
||||
AppFactory::setContainer($container);
|
||||
$app = AppFactory::create();
|
||||
|
||||
// Allow body parsing for POST parameters
|
||||
$app->addBodyParsingMiddleware();
|
||||
|
||||
// Add Error Handling Middleware
|
||||
$app->addErrorMiddleware(true, false, false);
|
||||
|
||||
|
10
src/database.php
Normal file
10
src/database.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
$capsule = new \Illuminate\Database\Capsule\Manager;
|
||||
$capsule->addConnection($config->get('database'));
|
||||
$capsule->setAsGlobal();
|
||||
$capsule->bootEloquent();
|
||||
|
||||
$container->set('db', function () use ($capsule) {
|
||||
return $capsule;
|
||||
});
|
@ -6,3 +6,28 @@ use Slim\Views\Twig;
|
||||
|
||||
// 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');
|
||||
|
||||
// /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
|
||||
$app->get('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getCreate')->setName('ticket.create');
|
||||
$app->post('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postCreate');
|
||||
|
||||
// /ticket/edit routes - update a ticket!
|
||||
$app->get('/ticket/{ticket_id}/edit', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getEdit')->setName('ticket.edit');
|
||||
$app->post('/ticket/{ticket_id}/edit', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postEdit')->setName('ticket.edit');
|
||||
$app->post('/ticket/{ticket_id}/edit/{attr}', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postEditAttr')->setName('ticket.edit.attr');
|
||||
|
||||
// ticket deletion route
|
||||
$app->get('/ticket/{ticket_id}/delete', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getDelete')->setName('ticket.delete');
|
||||
|
||||
// /ticket/id route - displays ticket info to user
|
||||
$app->get('/ticket/{ticket_id}', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getView')->setName('ticket.view');
|
||||
|
||||
// add a comment to a ticket
|
||||
$app->post('/comment/add', '\\BitGoblin\\Goliath\\Controllers\\CommentController:postAdd')->setName('comment.add');
|
||||
|
@ -3,5 +3,37 @@
|
||||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>This is a test.</p>
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Welcome to Goliath!</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
{% if tickets|length > 0 %}
|
||||
<table class="columns twelve">
|
||||
<thead>
|
||||
<th>Title</th>
|
||||
<th>Severity</th>
|
||||
<th>Due date</th>
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ticket in tickets %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('ticket.view', {ticket_id: ticket.id}) }}">{{ ticket.title }}</a></td>
|
||||
<td>{{ ticket.severity }}</td>
|
||||
<td>{{ ticket.due_at ? ticket.due_at : 'N/a' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-pen-to-square"></i></a>
|
||||
<a href="{{ url_for('ticket.delete', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>There a no tickets in the database at this time.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -6,9 +6,25 @@
|
||||
<title>{% block title %}{% endblock %} | Goliath</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
|
||||
<link rel="stylesheet" href="/styles/darkmeyer.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
|
||||
<script src="/js/slepe.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<nav id="main-nav">
|
||||
<div class="nav-left">
|
||||
<ul class="nav-menu">
|
||||
<li>Goliath</li>
|
||||
<li class="nav-link"><a href="{{ url_for('index') }}">Home</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>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="main-wrapper" class="card container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
29
views/queue/create.twig
Normal file
29
views/queue/create.twig
Normal 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
44
views/queue/view.twig
Normal 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 %}
|
49
views/ticket/create.twig
Normal file
49
views/ticket/create.twig
Normal file
@ -0,0 +1,49 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block title %}Create new ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Create new ticket</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<form id="ticket-form" action="/ticket/create" method="POST" class="u-full-width">
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<label for="ticket_title">Title</label>
|
||||
<input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title">
|
||||
</div>
|
||||
<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>
|
||||
<input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due">
|
||||
</div>
|
||||
<div class="two columns">
|
||||
<label for="ticket_severity">Severity level</label>
|
||||
<select id="ticket_severity" class="u-full-width" name="ticket_severity">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="ticket_body">Ticket body</label>
|
||||
<textarea id="ticket_body" class="u-full-width" placeholder="Explain what this ticket is about..." name="ticket_body"></textarea>
|
||||
|
||||
<input class="button-primary u-full-width" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
41
views/ticket/edit.twig
Normal file
41
views/ticket/edit.twig
Normal file
@ -0,0 +1,41 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block title %}Editing ticket{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<h1>Editing "{{ ticket.title }}"</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="columns twelve">
|
||||
<form id="ticket-form" action="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}" method="POST" class="u-full-width">
|
||||
<div class="row">
|
||||
<div class="six columns">
|
||||
<label for="ticket_title">Title</label>
|
||||
<input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title" value="{{ ticket.title }}">
|
||||
</div>
|
||||
<div class="columns three">
|
||||
<label for="ticket_due">Due at</label>
|
||||
<input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due" value="{{ ticket.due_at }}">
|
||||
</div>
|
||||
<div class="three columns">
|
||||
<label for="ticket_severity">Severity level</label>
|
||||
<select id="ticket_severity" class="u-full-width" name="ticket_severity" value="{{ ticket.severity }}">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="ticket_body">Ticket body</label>
|
||||
<textarea id="ticket_body" class="u-full-width" placeholder="Explain what this ticket is about..." name="ticket_body">{{ ticket.body }}</textarea>
|
||||
|
||||
<input class="button-primary u-full-width" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
81
views/ticket/view.twig
Normal file
81
views/ticket/view.twig
Normal file
@ -0,0 +1,81 @@
|
||||
{% extends 'layout.twig' %}
|
||||
|
||||
{% block title %}{{ ticket.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<!-- ticket content -->
|
||||
<div class="nine columns">
|
||||
<div id="ticket-header" class="row">
|
||||
<div class="columns twelve">
|
||||
<h1 class="ticket-title">{{ ticket.title }}</h1>
|
||||
<h4 class="ticket-created">Created at: <span>{{ ticket.formatCreatedAt() }}</span></h4>
|
||||
<h4 class="ticket-updated">Last updated at: <span>{{ ticket.formatUpdatedAt() }}</span></h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ticket-body" class="row">
|
||||
<div class="columns twelve">
|
||||
{{ ticket.render() | raw }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ticket status column -->
|
||||
<div class="three columns">
|
||||
<ul class="ticket-attributes">
|
||||
<li class="ticket-actions">
|
||||
<ul>
|
||||
<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>
|
||||
</li>
|
||||
<li class="ticket-queue" data-id="{{ ticket.queue.id }}">
|
||||
Queue: <span>{{ ticket.queue.title }}</span>
|
||||
</li>
|
||||
<li class="ticket-severity">
|
||||
Severity: <span>{{ ticket.severity | capitalize }}</span>
|
||||
</li>
|
||||
<li class="ticket-status">
|
||||
Status: <span>{{ ticket.status | capitalize }}</span>
|
||||
</li>
|
||||
<li>
|
||||
Assignee(s): N/a
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- ticket updates -->
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<h3>Ticket comments/Updates</h3>
|
||||
{% if ticket.comments | length > 0 %}
|
||||
<ul class="comments-list">
|
||||
{% for comment in ticket.comments %}
|
||||
<li class="comment">{{ comment.render() | raw }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>There are no comments to display at this time.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- add a comment form -->
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<form id="comment-form" action="{{ url_for('comment.add') }}" method="POST" class="u-full-width">
|
||||
<label for="comment_body">Add a comment:</label>
|
||||
<textarea id="comment_body" class="u-full-width" placeholder="Add a comment..." name="comment_body"></textarea>
|
||||
|
||||
<input type="hidden" name="ticket_id" value="{{ ticket.id }}">
|
||||
|
||||
<input class="button-primary u-full-width" type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user