Compare commits

..

6 Commits

21 changed files with 693 additions and 149 deletions

View File

@ -46,6 +46,9 @@ POST /tag/create=TagController->create
; parsedown preview ; parsedown preview
POST /parsedown/preview=ParsedownPreview->view POST /parsedown/preview=ParsedownPreview->view
; toggle-theme
POST /toggle-theme = ThemeController->toggle
; dashboard ; dashboard
GET /dashboard=DashboardController->index GET /dashboard=DashboardController->index

View File

@ -0,0 +1,13 @@
<?php
class ThemeController
{
function toggle($f3)
{
$current = $f3->get('SESSION.theme') ?: 'light';
$new_theme = ($current === 'light') ? 'dark' : 'light';
$f3->set('SESSION.theme', $new_theme);
$f3->reroute($f3->get('HEADERS.Referer') ?: '/');
}
}

View File

@ -9,9 +9,16 @@ class TicketController extends BaseController implements CRUD {
$this->requireLogin(); $this->requireLogin();
$filter = $f3->get('GET.status');
// retrieve tickets // retrieve tickets
$ticket_mapper = new Ticket($this->getDB()); $ticket_mapper = new Ticket($this->getDB());
if($filter){
$tickets = $ticket_mapper->findFiltered($filter);
} else {
$tickets = $ticket_mapper->findAll(); $tickets = $ticket_mapper->findAll();
}
// render // render
$this->renderView('../ui/views/ticket/index.html', $this->renderView('../ui/views/ticket/index.html',
@ -99,6 +106,9 @@ class TicketController extends BaseController implements CRUD {
$this->f3->reroute('/tickets'); $this->f3->reroute('/tickets');
} }
//
$f3->set('js', 'markdown_preview.js');
// dropdowns // dropdowns
$priorities = (new TicketPriority($this->getDB()))->findAll(); $priorities = (new TicketPriority($this->getDB()))->findAll();
$statuses = (new TicketStatus($this->getDB()))->findAll(); $statuses = (new TicketStatus($this->getDB()))->findAll();

View File

@ -7,24 +7,30 @@ class BulmaFormHelper extends \Prefab {
const H_FIELD_SELECT = 3; const H_FIELD_SELECT = 3;
const H_FIELD_SELECT_NEW = 4; const H_FIELD_SELECT_NEW = 4;
const FIELD_INPUT = 10;
const FIELD_TEXTAREA = 11;
const FIELD_SELECT = 13;
static public function render($node) { static public function render($node) {
$attr = $node['@attrib'] ?? []; $attr = $node['@attrib'] ?? [];
$type = strtoupper($attr['type']) ?? null; $type = strtoupper($attr['type']) ?? null;
// all * // all *
$label = $attr['label']; $label = $attr['label'] ?? '';
$name = $attr['name']; $name = $attr['name'] ?? '';
$value = isset($attr['value']) ? $attr['value'] : ''; $value = $attr['value'] ?? '';
$class = $attr['class'] ?? '';
// select // select
$options = isset($attr['options']) ? $attr['options'] : ''; $options = $attr['options'] ?? [];
$selected = isset($attr['selected']) ? $attr['selected'] : ''; $selected = $attr['selected'] ?? [];
// // textarea
$rows = $attr['rows'] ?? '';
$label = \Template::instance()->build($label); $label = \Template::instance()->build($label);
$name = \Template::instance()->build($name); $name = \Template::instance()->build($name);
$value = \Template::instance()->build($value); $value = \Template::instance()->build($value);
$options_array = \Template::instance()->token($options); $selected = \Template::instance()->build($selected);
if(defined("BulmaFormHelper::$type")){ if(defined("BulmaFormHelper::$type")){
@ -43,6 +49,15 @@ class BulmaFormHelper extends \Prefab {
case BulmaFormHelper::H_FIELD_SELECT_NEW: case BulmaFormHelper::H_FIELD_SELECT_NEW:
return BulmaFormHelper::build_h_field_select_new($attr); return BulmaFormHelper::build_h_field_select_new($attr);
break; break;
case BulmaFormHelper::FIELD_INPUT:
return BulmaFormHelper::build_field_input($label, $name, $value, $class);
break;
case BulmaFormHelper::FIELD_TEXTAREA:
return BulmaFormHelper::build_field_textarea($label, $name, $value, $class, $rows);
break;
case BulmaFormHelper::FIELD_SELECT:
return BulmaFormHelper::build_field_select($attr);
break;
default: default:
return '<div class="notification is-danger">Error: Bulma CSS Form TYPE ('.$type.') not defined.</div>'; return '<div class="notification is-danger">Error: Bulma CSS Form TYPE ('.$type.') not defined.</div>';
break; break;
@ -54,6 +69,132 @@ class BulmaFormHelper extends \Prefab {
} }
static function build_field_input($label, $name, $value, $class, $rows=10){
$string_label = $label !== '' ? sprintf('<label class="label">%1$s</label>', $label) : '';
$string = '
<div class="field %4$s">
%1$s
<div class="control">
<input class="input" id="%2$s" name="%2$s" type="text" placeholder="" value="%3$s">
</div>
</div>
';
return sprintf($string, $string_label, $name, $value, $class, $rows);
}
static function build_field_textarea($label, $name, $value, $class, $rows=10)
{
$string_label = $label !== '' ? sprintf('<label class="label">%1$s</label>', $label) : '';
$string = '
<div class="field %4$s">
%1$s
<div class="control">
<textarea class="textarea" id="%2$s" name="%2$s" rows="%5$s">%3$s</textarea>
</div>
</div>
';
return sprintf($string, $string_label, $name, $value, $class,$rows);
}
static function build_h_field_textarea($label, $name, $value){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">'.$label.'</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" id="'.$name.'" name="'.
$name.'">'.$value.'</textarea>
</div>
</div>
</div>
</div>
';
return $string;
}
static function build_h_field_input($label, $name, $value){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">'.$label.'</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" id="'.$name.'" name="'.
$name.'" value="'.
$value.'">
</div>
</div>
</div>
</div>
';
return $string;
}
/**
* build_field_select_new
*
* `<bulma type="H_FIELD_SELECT" label="Priority:" name="priority_id"
* options="priorities" option_value="id" option_name="name"
* selected="{{@ticket.priority_id}}"></bulma>`
*
* @param mixed $attr
* @return void
*/
static function build_field_select($attr)
{
$f3 = \Base::instance();
$class = $attr['class'] ?? '';
$label = $attr['label'] ?? '';
$name = $attr['name'] ?? '';
// $options_arr = $attr['options'] ?? [];
$option_value = $attr['option_value'] ?? 'id';
$option_name = $attr['option_name'] ?? 'name';
$options = \Template::instance()->token($attr['options']);
$selected = \Template::instance()->token($attr['selected']);
// TODO: label - this could be moved into a seperate function
$html_label = $label !== '' ? sprintf('<label class="label">%1$s</label>', $label) : '';
$tmp_options = '<?php echo \BulmaFormHelper::instance()->field_select('.
$options.', '.$selected.', "'.$option_value.'", "'.$option_name.'"); ?>';
$html = '
<div class="field %4$s">
%1$s
<div class="control">
<div class="select">
<select name="%3$s" id="%3$s">
%2$s
</select>
</div>
</div>
</div>
';
return sprintf($html, $html_label, $tmp_options, $name, $class);
}
function field_select($options, $selected, $option_value, $option_name){
$html_options = '';
foreach ($options as $option) {
$value = $option[$option_value] ?? '';
$text = $option[$option_name] ?? '';
$html_selected = ((string)$value === (string)$selected) ? ' selected="selected"' : '';
$html_option = '<option value="%s"%s>%s</option>';
$html_options .= sprintf($html_option, $value, $html_selected, $text);
}
echo $html_options;
}
static function build_h_field_select_new($attr) static function build_h_field_select_new($attr)
{ {
$f3 = \Base::instance(); $f3 = \Base::instance();
@ -88,25 +229,7 @@ class BulmaFormHelper extends \Prefab {
return $html; return $html;
} }
static function build_h_field_input($label, $name, $value){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">'.$label.'</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" id="'.$name.'" name="'.
$name.'" value="'.
$value.'">
</div>
</div>
</div>
</div>
';
return $string;
}
static function build_h_field_select($label, $name, $options, $selected){ static function build_h_field_select($label, $name, $options, $selected){
$opts = json_decode(str_replace("'", '"', $options)); $opts = json_decode(str_replace("'", '"', $options));
@ -139,25 +262,6 @@ class BulmaFormHelper extends \Prefab {
return $string; return $string;
} }
static function build_h_field_textarea($label, $name, $value){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">'.$label.'</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" id="'.$name.'" name="'.
$name.'">'.$value.'</textarea>
</div>
</div>
</div>
</div>
';
return $string;
}
} }
\Template::instance()->extend('bulma', 'BulmaFormHelper::render'); \Template::instance()->extend('bulma', 'BulmaFormHelper::render');

View File

@ -3,10 +3,17 @@
class IconsHelper extends \Prefab { class IconsHelper extends \Prefab {
static public $status_icons = [ static public $status_icons = [
'New' => ['fas fa-circle-dot has-text-success', "🆕"], 'open' => ['fas fa-circle-dot has-text-success', "🆕"],
'In Progress' => ['fas fa-circle-play has-text-link', "🔄"], 'in_progress' => ['fas fa-circle-play has-text-link', "🔄"],
'On Hold' => ['fas fa-pause-circle has-text-warning',"⏸️"], 'on_hold' => ['fas fa-pause-circle has-text-warning',"⏸️"],
'Completed' => ['fas fa-check-circle has-text-danger', ""] 'completed' => ['fas fa-check has-text-danger', ""]
];
static public $status_names = [
'open' => 'Open',
'in_progress' => 'In Progress',
'on_hold' => 'On Hold',
'completed' => 'Completed'
]; ];
static public $priority_icons = [ static public $priority_icons = [
@ -38,6 +45,9 @@ class IconsHelper extends \Prefab {
static function do_the_switch($type, $value){ static function do_the_switch($type, $value){
if($value !== null) {
$value = str_replace(' ', '_', strtolower($value));
}
$icon_class = ''; $icon_class = '';
switch(strtolower($type)){ switch(strtolower($type)){
case 'status': case 'status':

View File

@ -2,27 +2,65 @@
class ParsedownTableExtension extends Parsedown class ParsedownTableExtension extends Parsedown
{ {
protected function blockTable($Line, ?array $Block = null) protected function blockTable($Line, array $Block = null)
{ {
// Let Parsedown do its normal 'start-of-table' parsing.
$Block = parent::blockTable($Line, $Block); $Block = parent::blockTable($Line, $Block);
if(!isset($Block)){
// If this line didn't create or start a table, do nothing.
if (!isset($Block)) {
return null; return null;
} }
// add classes to the table element // Flag it so we know in blockTableComplete that this is a table block.
$Block['element']['attributes'] = [ $Block['isMyTable'] = true;
'class' => 'table is-bordered',
];
// wrap the table in a bulma div return $Block;
$Block['element'] = [ }
protected function blockTableContinue($Line, array $Block)
{
// Continue letting Parsedown do its normal table parsing.
$Block = parent::blockTableContinue($Line, $Block);
return $Block;
}
protected function blockTableComplete(array $Block)
{
// Let Parsedown finalize the table structure.
// $Block = parent::blockTableComplete($Block);
// If we flagged this as our table block, wrap it now.
if (!empty($Block['isMyTable'])) {
// $Block['element'] should now be fully formed, e.g.:
// [
// 'name' => 'table',
// 'handler' => 'elements',
// 'text' => [ ... ],
// 'attributes' => [...],
// ]
// Add your custom class to the <table> itself:
if (!isset($Block['element']['attributes'])) {
$Block['element']['attributes'] = [];
}
$Block['element']['attributes']['class'] = 'table is-bordered';
// Wrap the <table> in a <div class="table-container">:
$wrapped = [
'name' => 'div', 'name' => 'div',
'attributes' => [ 'attributes' => [
'class' => 'table-container' 'class' => 'table-container',
],
'handler' => 'elements',
'text' => [
$Block['element'], // the <table> itself
], ],
'handler' => 'element',
'text' => $Block['element'],
]; ];
// Replace the original element with our wrapped version:
$Block['element'] = $wrapped;
}
return $Block; return $Block;
} }
} }

79
app/models/Tag.php Normal file
View File

@ -0,0 +1,79 @@
<?php
class Tag extends \DB\SQL\Mapper
{
protected $tag_table, $tag_table_id;
function __construct($db, $type = null)
{
if($type == null){
// do tag mapping
parent::__construct($db, 'tags');
} else {
$this->tag_table = $type . '_tags';
$this->tag_table_id = $type . '_id';
parent::__construct($db, $this->tag_table);
}
return $this;
}
// VERIFY: possible issue with this?
public function getTagsFor($objects, $id_key = 'id')
{
// echo $this->get('_type_id'); exit;
// printf('<pre>%s</pre>', print_r($this,1)); exit;
if(empty($objects)) return [];
$ids = array_column($objects, $id_key);
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql =
'SELECT tt.%1$s, t.id, t.name, t.color
FROM %2$s tt
INNER JOIN tags t ON tt.tag_id = t.id
WHERE tt.%1$s IN (%3$s)';
$sql_sprintf = sprintf($sql, $this->tag_table_id, $this->tag_table, $placeholders);
$rows = $this->db->exec($sql_sprintf, $ids);
$tags_map = [];
foreach($rows as $row)
{
$tags_map[$row[$this->tag_table_id]][] = $row;
}
foreach($objects as &$object)
{
$object['tags'] = $tags_map[$object[$id_key]] ?? [];
}
return $objects;
}
public function getTagsForID($id, $id_key = 'id')
{
$sql = 'SELECT tt.%1$s, t.id, t.name, t.color
FROM %2$s tt
INNER JOIN tags t ON tt.tag_id = t.id
WHERE tt.%1$s = ?';
$sql_sprintf = sprintf($sql, $this->tag_table_id, $this->tag_table);
$rows = $this->db->exec($sql_sprintf, $id);
return $rows;
}
public function findLinkedTags($id = '')
{
$sql = '
SELECT t.name, t.color
FROM `?` tt
LEFT JOIN `tags` t ON t.id = tt.id
WHERE tt.`?` = ?
';
$params = [
$this->_type,
$this->_type_id,
$id
];
return $this->db->exec($sql, $params);
}
}

View File

@ -3,6 +3,7 @@
class Ticket extends \DB\SQL\Mapper { class Ticket extends \DB\SQL\Mapper {
function __construct($db){ function __construct($db){
parent::__construct($db, 'tickets'); parent::__construct($db, 'tickets');
} }
/** /**
@ -10,8 +11,9 @@ class Ticket extends \DB\SQL\Mapper {
*/ */
public function findAll(): array public function findAll(): array
{ {
return $this->db->exec( $tickets = $this->db->exec(
'SELECT t.* , tp.name AS priority_name, ts.name AS status_name, u.display_name 'SELECT t.id, t.title, t.created_at,
tp.name AS priority_name, ts.name AS status_name, u.display_name
FROM tickets t FROM tickets t
LEFT JOIN ticket_priorities tp ON t.priority_id = tp.id LEFT JOIN ticket_priorities tp ON t.priority_id = tp.id
LEFT JOIN ticket_statuses ts ON t.status_id = ts.id LEFT JOIN ticket_statuses ts ON t.status_id = ts.id
@ -19,11 +21,60 @@ class Ticket extends \DB\SQL\Mapper {
WHERE t.recycled = 0 WHERE t.recycled = 0
ORDER BY t.created_at DESC' ORDER BY t.created_at DESC'
); );
$result = $this->getTagsForTickets($tickets);
return $result;
}
public function findFiltered(string $filter): array
{
$sql = '
SELECT t.*, tp.name AS priority_name, ts.name AS status_name, u.display_name
FROM tickets t
LEFT JOIN ticket_priorities tp ON t.priority_id = tp.id
LEFT JOIN ticket_statuses ts ON t.status_id = ts.id
LEFT JOIN users u ON t.created_by = u.id
WHERE t.recycled = 0
';
$params = [];
switch($filter){
case 'open':
$sql .= ' AND status_id = ?';
$params[] = 1;
break;
case 'in_progress':
$sql .= ' AND status_id = ?';
$params[] = 2;
break;
case 'on_hold':
$sql .= ' AND status_id = ?';
$params[] = 3;
break;
case 'completed':
$sql .= ' AND status_id = ?';
$params[] = 4;
break;
}
$sql .= ' ORDER BY t.created_at DESC';
$tickets = $this->db->exec($sql, $params);
$result = $this->getTagsForTickets($tickets);
return $result;
}
public function getTagsForTickets(array $tickets)
{
$tag_mapper = new Tag($this->db, 'ticket');
$tickets = $tag_mapper->getTagsFor($tickets);
return $tickets;
} }
public function findById($id): ?Ticket public function findById($id): ?Ticket
{ {
$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->load(['id = ?', $id]);
$this->tags = (new Tag($this->db,'ticket'))->getTagsForID($id, 'ticket_id');
return $this->dry() ? null : $this; return $this->dry() ? null : $this;
} }
@ -35,7 +86,7 @@ class Ticket extends \DB\SQL\Mapper {
$this->description = $data['description'] ?? ''; $this->description = $data['description'] ?? '';
// //
$this->priority_id = $data['priority_id'] ?? null; $this->priority_id = $data['priority_id'] ?? null;
$this->status = $data['status_id'] ?? null; $this->status_id = $data['status_id'] ?? null;
// //
$this->created_by = $data['created_by'] ?? null; $this->created_by = $data['created_by'] ?? null;
$this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s'); $this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,73 @@
class TabSwitcherController {
constructor({ tabSelector, contentPrefix, textareaSelector, previewUrl }) {
this.tabSelector = tabSelector;
this.contentPrefix = contentPrefix;
this.textareaSelector = textareaSelector;
this.previewUrl = previewUrl;
this.tabParent = document.querySelector(tabSelector);
this.tabLinks = this.tabParent.querySelectorAll('a');
this.init();
}
init() {
this.tabLinks.forEach(link => {
link.addEventListener('click', (e) => this.handleTabClick(e, link));
});
}
async handleTabClick(e, link) {
e.preventDefault();
const selectedTab = link.getAttribute('data-tab');
// Update active tab
this.tabParent.querySelectorAll('li').forEach(li => li.classList.remove('is-active'));
link.parentElement.classList.add('is-active');
// Show active content
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
const activeContent = document.getElementById(`${this.contentPrefix}-${selectedTab}`);
if (activeContent) activeContent.style.display = '';
if (selectedTab === 'preview') {
await this.loadPreview();
}
}
async loadPreview() {
const previewTarget = document.getElementById('preview-output');
if (!previewTarget) return;
previewTarget.innerHTML = `
<div class="skeleton-lines">
<div></div><div></div><div></div><div></div><div></div>
</div>
`;
await new Promise(resolve => setTimeout(resolve, 500));
const textarea = document.querySelector(this.textareaSelector);
const markdown = textarea ? textarea.value : '';
const res = await fetch(this.previewUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `content=${encodeURIComponent(markdown)}`
});
const html = await res.text();
previewTarget.innerHTML = html;
}
}
// Usage
document.addEventListener('DOMContentLoaded', () => {
new TabSwitcherController({
tabSelector: '.tabs',
contentPrefix: 'tab',
textareaSelector: '#description',
previewUrl: '/parsedown/preview'
});
});

View File

@ -28,12 +28,13 @@
@extend .is-flex; @extend .is-flex;
@extend .is-justify-content-flex-start; @extend .is-justify-content-flex-start;
@extend .is-flex-wrap-wrap; @extend .is-flex-wrap-wrap;
@extend .mb-1;
align-items: center; align-items: center;
.ticket-title { .ticket-title {
@extend .title; @extend .title;
@extend .mb-0;
@extend .is-5; @extend .is-5;
@extend .mb-1;
font-weight: normal; font-weight: normal;
} }

View File

@ -15,16 +15,10 @@
<span class="ticket-title"> <span class="ticket-title">
<a href="/ticket/{{ @ticket.id }}">{{ @ticket.title }}</a> <a href="/ticket/{{ @ticket.id }}">{{ @ticket.title }}</a>
</span> </span>
<div class="tags">
<div class="tags ml-2"> <div class="tags ml-2">
<!-- TODO: get tags -->
<span class="tag is-link">tag</span>
</div>
<?php /*
<repeat group="{{ @ticket.tags }}" value="{{ @tag }}"> <repeat group="{{ @ticket.tags }}" value="{{ @tag }}">
<span class="tag is-link">{{ @tag }}</span> <span class="tag is-{{@tag.color}}">{{ @tag.name }}</span>
</repeat> </repeat>
*/ ?>
</div> </div>
</div> </div>
<div class="ticket-meta"> <div class="ticket-meta">

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html data-theme="{{ @SESSION.theme ?? 'light' }}" lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
@ -52,6 +52,13 @@
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<form id="theme-toggle-form" method="post" action="/toggle-theme" style="display:inline">
<button type="submit" id="theme-toggle-button" class="button is-small" aria-label="Toggle Theme">
<span class="icon">
<i class="fas fa-circle-half-stroke" id="theme-icon"></i>
</span>
</button>
</form>
<check if="{{ isset(@SESSION.user) }}"> <check if="{{ isset(@SESSION.user) }}">
<true> <true>
<a class="button is-primary" href="/logout">Log Out</a> <a class="button is-primary" href="/logout">Log Out</a>

View File

@ -1,6 +1,6 @@
<div class="box"> <div class="box">
<div class="content"> <div class="content">
<h2 class="title">Attachments</h2> <h4 class="title is-4">Attachments</h4>
<div class="block"> <div class="block">
<check if="isset( {{@attachments }})"> <check if="isset( {{@attachments }})">
<check if="count({{@attachments}}) > 0"> <check if="count({{@attachments}}) > 0">
@ -37,7 +37,6 @@
</check> </check>
</check> </check>
<div class="block"> <div class="block">
<h3 class="title">Upload attachment</h3>
<form action="/ticket/{{@PARAMS.id}}/attachments/upload" method="POST" enctype="multipart/form-data"> <form action="/ticket/{{@PARAMS.id}}/attachments/upload" method="POST" enctype="multipart/form-data">
<div class="field has-addons"> <div class="field has-addons">
<div class="control has-icons-left"><!-- is-expanded --> <div class="control has-icons-left"><!-- is-expanded -->

View File

@ -1,6 +1,6 @@
<hr> <hr>
<div class="box" id="comments"> <div class="box" id="comments">
<h2 class="title">Comments</h2> <h3 class="title is-3">Comments</h3>
<check if="{{ !empty(@comments) }}"> <check if="{{ !empty(@comments) }}">
<div class="list"> <div class="list">
<repeat group="{{ @comments }}" value="{{ @comment}}"> <repeat group="{{ @comments }}" value="{{ @comment}}">

View File

@ -6,7 +6,7 @@
<bulma type="H_FIELD_INPUT" label="Created At:" name="created_at" value=""></bulma> <bulma type="H_FIELD_INPUT" label="Created At:" name="created_at" value=""></bulma>
<bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value=""></bulma> <bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value=""></bulma>
<!-- TODO implement priority and status --> <!-- priority and status -->
<bulma type="H_FIELD_SELECT_NEW" label="Priority:" name="priority_id" <bulma type="H_FIELD_SELECT_NEW" label="Priority:" name="priority_id"
options="priorities" option_value="id" option_name="name" options="priorities" option_value="id" option_name="name"
selected="2"></bulma> selected="2"></bulma>

View File

@ -1,60 +1,141 @@
<h1 class="title">Edit Ticket Form</h1> <!-- Ticket - Edit -->
<!-- made to look more in line with view-->
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST"> <form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
<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">
<!-- <p class="control"><a class="button" href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
<p class="control"><a class="button is-primary" href="/ticket/create">new ticket</a></p>-->
<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>
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@ticket.title}}"></bulma> <hr>
<bulma type="H_FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}"></bulma> <div class="block">
<bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value="{{@ticket.description}}"></bulma>
<div class="columns">
<div class="column is-two-thirds">
<bulma type="H_FIELD_SELECT_NEW" label="Priority:" name="priority_id" <div class="block">
options="priorities" option_value="id" option_name="name" <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 id="preview-output"></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> selected="{{@ticket.priority_id}}"></bulma>
<bulma type="H_FIELD_SELECT_NEW" label="Status:" name="status_id" <bulma type="FIELD_SELECT" label="Status:" name="status_id"
options="statuses" option_value="id" option_name="name" options="{{@statuses}}" option_value="id" option_name="name"
selected="@ticket.status_id"></bulma> selected="{{@ticket.status_id}}"></bulma>
</div>
<include href="/ui/parts/clipboard.html"></include> <!-- meta data -->
<div class="block"> <div class="block">
<h3 class="title is-5">Custom Fields</h3> <table class="table is-bordered is-fullwidth">
<!-- existing fields--> <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"> <div class="block">
<repeat group="{{ @ticket_meta }}" value="{{ @m }}"> <h4 class="title">Parent Tickets</h4>
<div class="field is-grouped is-grouped-right"> <ul>
<input type="hidden" name="meta_id[]" value=" {{ @m.id }}"> <repeat group="{{ @parent_tickets }}" value="{{ @p }}">
<div class="control"> <li><a href="/ticket/{{ @p.id }}">{{ @p.title }}</a></li>
<label class="label">Key:</label> </repeat>
<input class="input" type="text" name="meta_key[]" value="{{ @m.meta_key }}" </ul>
placeholder="eg. Department">
</div>
<div class="control">
<label class="label">Value:</label>
<input class="input" type="text" name="meta_value[]" value="{{ @m.meta_value }}"
placeholder="eg. Finance">
</div>
</div> </div>
</check>
<!-- child tickets -->
<check if="{{ @child_tickets }}">
<div class="block">
<h4 class="title">Child Tickets</h4>
<ul>
<repeat group="{{ @child_tickets }}" value="{{ @c }}">
<li><a href="/ticket/{{ @c.id }}">{{ @c.title }}</a></li>
</repeat> </repeat>
</div> </div>
<hr> </check>
<!-- adding new custom meta -->
<div class="block">
<div class="field is-grouped is-grouped-right">
<div class="control">
<label class="label">Key:</label>
<input class="input" type="text" name="meta_key[]"
placeholder="eg. Department">
</div>
<div class="control">
<label class="label">Value:</label>
<input class="input" type="text" name="meta_value[]"
placeholder="eg. Finance">
</div>
</div>
</div>
</div>
<button class="button is-primary" type="submit">Save Ticket</button> <form action="/ticket/{{ @ticket.id }}/add-subtask" method="POST">
<div class="field">
<label class="label">Add existing ticket as child ticket (ID):</label>
<div class="control">
<input class="input" type="number" placeholder="Child Ticket ID" required
name="child_ticket_id">
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-link" type="submit">Link</button>
</div>
</div>
</form>
*/ ?>
</div>
</div>
</div>
</div> </div>
</form> </form>
<hr>
<div>
<include href="../ui/views/attachment/index.html">
<include href="../ui/views/comments/view.html">
<!--
<div class="block" id="attachments"></div>
<div class="block" id="comments"></div>
-->
</div>

View File

@ -0,0 +1,60 @@
<h1 class="title">Edit Ticket Form</h1>
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@ticket.title}}"></bulma>
<bulma type="H_FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}"></bulma>
<bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value="{{@ticket.description}}"></bulma>
<bulma type="H_FIELD_SELECT_NEW" label="Priority:" name="priority_id"
options="priorities" option_value="id" option_name="name"
selected="{{@ticket.priority_id}}"></bulma>
<bulma type="H_FIELD_SELECT_NEW" label="Status:" name="status_id"
options="statuses" option_value="id" option_name="name"
selected="{{@ticket.status_id}}"></bulma>
<include href="/ui/parts/clipboard.html"></include>
<div class="block">
<h3 class="title is-5">Custom Fields</h3>
<!-- existing fields-->
<div class="block">
<repeat group="{{ @ticket_meta }}" value="{{ @m }}">
<div class="field is-grouped is-grouped-right">
<input type="hidden" name="meta_id[]" value=" {{ @m.id }}">
<div class="control">
<label class="label">Key:</label>
<input class="input" type="text" name="meta_key[]" value="{{ @m.meta_key }}"
placeholder="eg. Department">
</div>
<div class="control">
<label class="label">Value:</label>
<input class="input" type="text" name="meta_value[]" value="{{ @m.meta_value }}"
placeholder="eg. Finance">
</div>
</div>
</repeat>
</div>
<hr>
<!-- adding new custom meta -->
<div class="block">
<div class="field is-grouped is-grouped-right">
<div class="control">
<label class="label">Key:</label>
<input class="input" type="text" name="meta_key[]"
placeholder="eg. Department">
</div>
<div class="control">
<label class="label">Value:</label>
<input class="input" type="text" name="meta_value[]"
placeholder="eg. Finance">
</div>
</div>
</div>
</div>
<button class="button is-primary" type="submit">Save Ticket</button>
</div>
</form>

View File

@ -2,22 +2,38 @@
<include href="/ui/session/error.html"></include> <include href="/ui/session/error.html"></include>
</include> </include>
<!-- updated design -- inspiration gitea --> <!-- updated design -- inspiration gitea -->
<div class="field is-grouped"> <div class="block">
<div class="field is-grouped">
<div class="control is-expanded"> <div class="control is-expanded">
<div class="field has-addons is-expanded"> <div class="field has-addons is-expanded">
<div class="control is-expanded"> <div class="control is-expanded">
<input class="input" type="text" placeholder="Find a ticket"> <input class="input" type="text" placeholder="Find a ticket">
</div> </div>
<div class="control"> <div class="control">
<button class="button is-info"><span class="icon"><i class="fas fa-magnifying-glass"></i></span></button> <button class="button is-info"><span class="icon"><i
class="fas fa-magnifying-glass"></i></span></button>
</div> </div>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<p><a class="button is-primary" href="/ticket/create">create ticket</a></p> <p><a class="button is-primary" href="/ticket/create">create ticket</a></p>
</div> </div>
</div>
</div>
</div> </div>
<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> </div>
<hr> <hr>

View File

@ -24,6 +24,11 @@
<div class="column"> <div class="column">
<!-- meta data --> <!-- meta data -->
<div class="block"> <div class="block">
<div class="tags">
<repeat group="{{ @ticket.tags }}" value="{{@tag}}">
<span class="tag is-{{@tag.color}}">{{@tag.name}}</span>
</repeat>
</div>
<table class="table is-bordered is-fullwidth"> <table class="table is-bordered is-fullwidth">
<thead> <thead>
<tr><th class="has-width-100">Property</th><th>Value</th></tr> <tr><th class="has-width-100">Property</th><th>Value</th></tr>