Compare commits
3 Commits
2a3465fff8
...
164692a728
| Author | SHA1 | Date | |
|---|---|---|---|
| 164692a728 | |||
| 53bce00c65 | |||
| 8c8de5ec34 |
@ -28,6 +28,7 @@ GET /ticket/@id/attachments=AttachmentController->index
|
|||||||
POST /ticket/@id/attachments/upload=AttachmentController->upload
|
POST /ticket/@id/attachments/upload=AttachmentController->upload
|
||||||
GET /attachment/@id/download=AttachmentController->download
|
GET /attachment/@id/download=AttachmentController->download
|
||||||
GET /attachment/@id/delete=AttachmentController->delete
|
GET /attachment/@id/delete=AttachmentController->delete
|
||||||
|
GET /attachment/@id/view=AttachmentController->view
|
||||||
|
|
||||||
; knowledgebase
|
; knowledgebase
|
||||||
GET /kb=KBController->index
|
GET /kb=KBController->index
|
||||||
|
|||||||
@ -145,4 +145,41 @@ class AttachmentController {
|
|||||||
// remove DB row
|
// remove DB row
|
||||||
$db->exec('DELETE FROM attachments WHERE id =?', [$attachment_id]);
|
$db->exec('DELETE FROM attachments WHERE id =?', [$attachment_id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// view attachment
|
||||||
|
public function view($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$attachment_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
$rows = $db->exec('SELECt * FROM attachments WHERE id = ?', [$attachment_id]);
|
||||||
|
|
||||||
|
if(!$rows){
|
||||||
|
$f3->error(404, "File not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = $rows[0];
|
||||||
|
$file_path = $attachment['path'];
|
||||||
|
$file_name = $attachment['file_name'];
|
||||||
|
|
||||||
|
if(!file_exists($file_path)){
|
||||||
|
$f3->error(404, "File not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect mime type
|
||||||
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
$mime_type = finfo_file($finfo, $file_path);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
|
header('Content-Type: ' . $mime_type);
|
||||||
|
header('Content-Disposition: inline; filename="' . basename($file_name) . '"');
|
||||||
|
header('Content-Length: ' . filesize($file_path));
|
||||||
|
|
||||||
|
flush();
|
||||||
|
readfile($file_path);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -58,6 +58,7 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => $this->f3->get('POST.title'),
|
'title' => $this->f3->get('POST.title'),
|
||||||
|
'created_at' => $this->f3->get('POST.created_at'),
|
||||||
'description' => $this->f3->get('POST.description'),
|
'description' => $this->f3->get('POST.description'),
|
||||||
'priority' => $this->f3->get('POST.priority'),
|
'priority' => $this->f3->get('POST.priority'),
|
||||||
'status' => $this->f3->get('POST.status'),
|
'status' => $this->f3->get('POST.status'),
|
||||||
@ -115,6 +116,7 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => $this->f3->get('POST.title'),
|
'title' => $this->f3->get('POST.title'),
|
||||||
|
'created_at' => $this->f3->get('POST.created_at'),
|
||||||
'description' => $this->f3->get('POST.description'),
|
'description' => $this->f3->get('POST.description'),
|
||||||
'priority' => $this->f3->get('POST.priority'),
|
'priority' => $this->f3->get('POST.priority'),
|
||||||
'status' => $this->f3->get('POST.status'),
|
'status' => $this->f3->get('POST.status'),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class ParsedownHelper extends \Prefab {
|
|||||||
|
|
||||||
$return = \Parsedown::instance()->text($args[0]);
|
$return = \Parsedown::instance()->text($args[0]);
|
||||||
|
|
||||||
return '
|
return '<!-- tableextension -->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<parsedown_rendered>'.$return.'</parsedown_rendered>
|
<parsedown_rendered>'.$return.'</parsedown_rendered>
|
||||||
</div>
|
</div>
|
||||||
@ -28,7 +28,7 @@ class ParsedownHelper extends \Prefab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function build($content){
|
function build($content){
|
||||||
return Parsedown::instance()->text($content);
|
return \ParsedownTableExtension::instance()->text($content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
app/extensions/ParsedownTableExtension.php
Normal file
28
app/extensions/ParsedownTableExtension.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ParsedownTableExtension extends Parsedown
|
||||||
|
{
|
||||||
|
protected function blockTable($Line, ?array $Block = null)
|
||||||
|
{
|
||||||
|
$Block = parent::blockTable($Line, $Block);
|
||||||
|
if(!isset($Block)){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add classes to the table element
|
||||||
|
$Block['element']['attributes'] = [
|
||||||
|
'class' => 'table is-bordered',
|
||||||
|
];
|
||||||
|
|
||||||
|
// wrap the table in a bulma div
|
||||||
|
$Block['element'] = [
|
||||||
|
'name' => 'div',
|
||||||
|
'attributes' => [
|
||||||
|
'class' => 'table-container'
|
||||||
|
],
|
||||||
|
'handler' => 'element',
|
||||||
|
'text' => $Block['element'],
|
||||||
|
];
|
||||||
|
return $Block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
$this->priority = $data['priority'] ?? 'Low';
|
$this->priority = $data['priority'] ?? 'Low';
|
||||||
$this->status = $data['status'] ?? 'New';
|
$this->status = $data['status'] ?? 'New';
|
||||||
$this->created_by = $data['created_by'] ?? null;
|
$this->created_by = $data['created_by'] ?? null;
|
||||||
$this->created_at = date('Y-m-d H:i:s');
|
$this->created_at = $data['created_at'] ?? date('Y-m-d H:i:s');
|
||||||
$this->updated_at = date('Y-m-d H:i:s');
|
$this->updated_at = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
@ -43,10 +43,11 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
public function updateTicket(array $data): void
|
public function updateTicket(array $data): void
|
||||||
{
|
{
|
||||||
if(isset($data['title'])){ $this->title = $data['title']; }
|
if(isset($data['title'])){ $this->title = $data['title']; }
|
||||||
if(isset($data['description'])) { $this->descrtipion = $data['description']; }
|
if(isset($data['description'])) { $this->description = $data['description']; }
|
||||||
if(isset($data['priority'])) { $this->priority = $data['priority']; }
|
if(isset($data['priority'])) { $this->priority = $data['priority']; }
|
||||||
if(isset($data['status'])) { $this->status = $data['status']; }
|
if(isset($data['status'])) { $this->status = $data['status']; }
|
||||||
if(isset($data['updated_by'])) { $this->status = $data['updated_by']; }
|
if(isset($data['updated_by'])) { $this->updated_by = $data['updated_by']; }
|
||||||
|
$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');
|
$this->updated_at = date('Y-m-d H:i:s');
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,18 +3,18 @@
|
|||||||
require '../lib/autoload.php';
|
require '../lib/autoload.php';
|
||||||
|
|
||||||
$f3 = \Base::instance();
|
$f3 = \Base::instance();
|
||||||
|
$f3->config('../app/config/.env.cfg');
|
||||||
|
$f3->set('DEBUG', 3); // development debug
|
||||||
|
$f3->set('CACHE', FALSE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Not required yet
|
* Not required yet
|
||||||
*/
|
*/
|
||||||
$htmlpurifier = \HTMLPurifier::instance();
|
$htmlpurifier = \HTMLPurifier::instance();
|
||||||
// $htmlpurifier->purify($input);
|
// $htmlpurifier->purify($input);
|
||||||
$md = \Parsedown::instance();
|
$md = \ParsedownTableExtension::instance();
|
||||||
$md->setSafeMode(true);
|
$md->setSafeMode(true);
|
||||||
|
|
||||||
$f3->config('../app/config/.env.cfg');
|
|
||||||
$f3->set('DEBUG', 3); // development debug
|
|
||||||
$f3->set('CACHE', FALSE);
|
|
||||||
|
|
||||||
$f3->set('EXT', [new ParsedownHelper, new BulmaFormHelper]);
|
$f3->set('EXT', [new ParsedownHelper, new BulmaFormHelper]);
|
||||||
|
|
||||||
$f3->set('DB', new \DB\SQL(
|
$f3->set('DB', new \DB\SQL(
|
||||||
|
|||||||
89
ui/parts/clipboard.html
Normal file
89
ui/parts/clipboard.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<div class="block">
|
||||||
|
<style>
|
||||||
|
#upload-area {
|
||||||
|
height: 250px;
|
||||||
|
border: 2px dashed #aaa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #555;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-area.hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
max-width:100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="upload-area" contenteditable="true">
|
||||||
|
Paste or drag an image here
|
||||||
|
</div>
|
||||||
|
<img id="preview" alt="image preview" hidden>
|
||||||
|
<p id="status"></p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const area = document.getElementById('upload-area');
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
|
||||||
|
async function uploadImage(file){
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
preview.src = reader.result;
|
||||||
|
preview.hidden = false;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachment', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/ticket/{{@ticket->id}}/attachments/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const text = await res.text();
|
||||||
|
status.textContent = text;
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = 'upload failed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// paste
|
||||||
|
area.addEventListener('paste', (e) => {
|
||||||
|
for(let item of e.clipboardData.items){
|
||||||
|
if(item.type.startsWith('image/')){
|
||||||
|
uploadImage(item.getAsFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// drag and drop
|
||||||
|
area.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
area.classList.add('hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
area.addEventListener('dragLeave', ()=> {
|
||||||
|
area.classList.remove('hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
area.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
area.classList.remove('hover');
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
for(let file of files){
|
||||||
|
if(file.type.startsWith('image/')){
|
||||||
|
uploadImage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@ -28,6 +28,12 @@
|
|||||||
</repeat>
|
</repeat>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<repeat group="{{ @attachments }}" value="{{ @attach }}">
|
||||||
|
<img src="/attachment/{{@attach.id}}/view">
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
</check>
|
</check>
|
||||||
</check>
|
</check>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
|
|||||||
@ -4,9 +4,10 @@
|
|||||||
<form action="/ticket/create" method="POST">
|
<form action="/ticket/create" method="POST">
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" 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 -->
|
||||||
<bulma type="H_FIELD_SELECT" label="Priority:" name="priority" options="['Low', 'Medium', 'High']" selected="Medium"></bulma>
|
<bulma type="H_FIELD_SELECT" label="Priority:" name="priority" options="['Low', 'Medium', 'High']" selected="Medium"></bulma>
|
||||||
<bulma type="H_FIELD_SELECT" label="Status:" name="status" options="['New', 'In Progress', 'On Hold', 'Completed']" selected="New"></bulma>
|
<bulma type="H_FIELD_SELECT" label="Status:" name="status" options="['New', 'In Progress', 'On Hold', 'Completed']" selected="New"></bulma>
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,14 @@
|
|||||||
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
<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="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_TEXTAREA" label="Description:" name="description" value="{{@ticket.description}}"></bulma>
|
||||||
|
|
||||||
|
|
||||||
<bulma type="H_FIELD_SELECT" label="Priority:" name="priority" options="['Low', 'Medium', 'High']" selected="{{@ticket.priority}}"></bulma>
|
<bulma type="H_FIELD_SELECT" label="Priority:" name="priority" options="['Low', 'Medium', 'High']" selected="{{@ticket.priority}}"></bulma>
|
||||||
<bulma type="H_FIELD_SELECT" label="Status:" name="status" options="['New', 'In Progress', 'On Hold', 'Completed']" selected="{{@ticket.status}}"></bulma>
|
<bulma type="H_FIELD_SELECT" label="Status:" name="status" options="['New', 'In Progress', 'On Hold', 'Completed']" selected="{{@ticket.status}}"></bulma>
|
||||||
|
|
||||||
|
<include href="/ui/parts/clipboard.html"></include>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-5">Custom Fields</h3>
|
<h3 class="title is-5">Custom Fields</h3>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user