mirror of
https://github.com/chylex/Lightning-Tracker.git
synced 2025-04-10 02:15:43 +02:00
Add issue checkbox syntax w/ update form
This commit is contained in:
parent
50a04f80e6
commit
615623b9c0
res/~resources/css
src
Database/Tables
Pages
Components/Markdown
Controllers/Tracker
Models/Tracker
Views/Tracker
@ -48,3 +48,7 @@
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.issue-description input[type=checkbox] {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
@ -117,6 +117,30 @@ SQL
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function updateIssueTasks(int $id, string $description, int $progress): void{
|
||||
if ($progress === 100){
|
||||
$status_from = 'in-progress';
|
||||
$status_to = 'ready-to-test';
|
||||
}
|
||||
else{
|
||||
$status_from = 'open';
|
||||
$status_to = 'in-progress';
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare(<<<SQL
|
||||
UPDATE issues
|
||||
SET description = ?, progress = ?, status = IF(status = '$status_from', '$status_to', status)
|
||||
WHERE issue_id = ? AND tracker_id = ?
|
||||
SQL
|
||||
);
|
||||
|
||||
$stmt->bindValue(1, $description);
|
||||
$stmt->bindValue(2, $progress, PDO::PARAM_INT);
|
||||
$stmt->bindValue(3, $id, PDO::PARAM_INT);
|
||||
$stmt->bindValue(4, $this->getTrackerId(), PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function countIssues(?IssueFilter $filter = null): ?int{
|
||||
$filter = $this->prepareFilter($filter ?? IssueFilter::empty());
|
||||
|
||||
@ -206,6 +230,14 @@ SQL
|
||||
$res['assignee_id'] === null ? null : new IssueUser($res['assignee_id'], $res['assignee_name']));
|
||||
}
|
||||
|
||||
public function getIssueDescription(int $id): string{
|
||||
$stmt = $this->db->prepare('SELECT description FROM issues WHERE issue_id = ? AND tracker_id = ?');
|
||||
$stmt->bindValue(1, $id, PDO::PARAM_INT);
|
||||
$stmt->bindValue(2, $this->getTrackerId(), PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $this->fetchOneColumn($stmt);
|
||||
}
|
||||
|
||||
public function deleteById(int $id): void{
|
||||
$stmt = $this->db->prepare('DELETE FROM issues WHERE issue_id = ? AND tracker_id = ?');
|
||||
$stmt->bindValue(1, $id, PDO::PARAM_INT);
|
||||
|
@ -8,29 +8,36 @@ use function Database\protect;
|
||||
|
||||
final class MarkdownComponent implements IViewable{
|
||||
private string $text;
|
||||
private ?string $checkbox_name = null;
|
||||
|
||||
public function __construct(string $text){
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function setCheckboxNameForEditing(string $checkbox_name): MarkdownComponent{
|
||||
$this->checkbox_name = $checkbox_name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRawTextSafe(): string{
|
||||
return protect($this->text);
|
||||
}
|
||||
|
||||
public function echoBody(): void{
|
||||
$parser = new MarkdownParser();
|
||||
public function parse(): MarkdownParseResult{
|
||||
$parser = new MarkdownParser($this->checkbox_name);
|
||||
$iter = new UnicodeIterator();
|
||||
|
||||
$lines = mb_split("\n", $this->text);
|
||||
$output = '';
|
||||
|
||||
foreach($lines as $line){
|
||||
$iter->prepare($line);
|
||||
$parser->parseLine($iter, $output);
|
||||
$parser->parseLine($iter);
|
||||
}
|
||||
|
||||
$parser->closeParser($output);
|
||||
echo $output;
|
||||
return $parser->closeParser();
|
||||
}
|
||||
|
||||
public function echoBody(): void{
|
||||
$this->parse()->echoBody();
|
||||
}
|
||||
}
|
||||
|
||||
|
26
src/Pages/Components/Markdown/MarkdownParseResult.php
Normal file
26
src/Pages/Components/Markdown/MarkdownParseResult.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Pages\Components\Markdown;
|
||||
|
||||
use Pages\IViewable;
|
||||
|
||||
final class MarkdownParseResult implements IViewable{
|
||||
private string $html;
|
||||
private int $checkboxes;
|
||||
|
||||
public function __construct(string $html, int $checkboxes){
|
||||
$this->html = $html;
|
||||
$this->checkboxes = $checkboxes;
|
||||
}
|
||||
|
||||
public function hasCheckboxes(): bool{
|
||||
return $this->checkboxes > 0;
|
||||
}
|
||||
|
||||
public function echoBody(): void{
|
||||
echo $this->html;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -8,12 +8,25 @@ use function Database\protect;
|
||||
final class MarkdownParser{
|
||||
private const SPACE = 32;
|
||||
private const HASH = 35;
|
||||
private const LEFT_SQUARE_BRACKET = 91;
|
||||
private const RIGHT_SQUARE_BRACKET = 93;
|
||||
private const LOWERCASE_X = 120;
|
||||
private const UPPERCASE_X = 88;
|
||||
|
||||
private string $output = '';
|
||||
|
||||
private ?string $checkbox_name;
|
||||
private int $checkbox_count = 0;
|
||||
|
||||
private bool $last_line_empty = false;
|
||||
private bool $is_paragraph_open = false;
|
||||
|
||||
public function __construct(?string $checkbox_name = null){
|
||||
$this->checkbox_name = $checkbox_name;
|
||||
}
|
||||
|
||||
/** @noinspection HtmlMissingClosingTag */
|
||||
public function parseLine(UnicodeIterator $iter, string &$output): void{
|
||||
public function parseLine(UnicodeIterator $iter): void{
|
||||
if (!$iter->valid()){
|
||||
$this->last_line_empty = true;
|
||||
return;
|
||||
@ -22,8 +35,17 @@ final class MarkdownParser{
|
||||
$heading = $this->parseHeading($iter);
|
||||
|
||||
if ($heading !== null){
|
||||
$this->closeParagraph($output);
|
||||
$output .= $heading;
|
||||
$this->closeParagraph();
|
||||
$this->output .= $heading;
|
||||
return;
|
||||
}
|
||||
|
||||
$iter->reset();
|
||||
$checkbox = $this->parseCheckBox($iter);
|
||||
|
||||
if ($checkbox !== null){
|
||||
$this->closeParagraph();
|
||||
$this->output .= $checkbox;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -36,28 +58,29 @@ final class MarkdownParser{
|
||||
}
|
||||
|
||||
if ($this->last_line_empty){
|
||||
$this->closeParagraph($output);
|
||||
$this->closeParagraph();
|
||||
}
|
||||
|
||||
if (!$this->is_paragraph_open){
|
||||
$output .= '<p>';
|
||||
$this->output .= '<p>';
|
||||
}
|
||||
else{
|
||||
$output .= ' ';
|
||||
$this->output .= ' ';
|
||||
}
|
||||
|
||||
$output .= $rest;
|
||||
|
||||
$this->output .= $rest;
|
||||
$this->is_paragraph_open = true;
|
||||
}
|
||||
|
||||
public function closeParser(string &$output): void{
|
||||
$this->closeParagraph($output);
|
||||
public function closeParser(): MarkdownParseResult{
|
||||
$this->closeParagraph();
|
||||
return new MarkdownParseResult($this->output, $this->checkbox_count);
|
||||
}
|
||||
|
||||
private function closeParagraph(string &$output): void{
|
||||
private function closeParagraph(): void{
|
||||
if ($this->is_paragraph_open){
|
||||
$this->is_paragraph_open = false;
|
||||
$output .= '</p>';
|
||||
$this->output .= '</p>';
|
||||
}
|
||||
|
||||
$this->last_line_empty = false;
|
||||
@ -71,7 +94,7 @@ final class MarkdownParser{
|
||||
}
|
||||
|
||||
$count = 1;
|
||||
|
||||
|
||||
foreach($iter as $code){
|
||||
if ($code === self::HASH){
|
||||
$count++;
|
||||
@ -94,6 +117,55 @@ final class MarkdownParser{
|
||||
return "<$tag>$rest</$tag>";
|
||||
}
|
||||
|
||||
private function parseCheckBox(UnicodeIterator $iter): ?string{
|
||||
if ($iter->move() !== self::LEFT_SQUARE_BRACKET){
|
||||
return null;
|
||||
}
|
||||
|
||||
$next = $iter->move();
|
||||
$checked = false;
|
||||
|
||||
if ($next === self::LOWERCASE_X || $next === self::UPPERCASE_X){
|
||||
$checked = true;
|
||||
$next = $iter->move();
|
||||
}
|
||||
elseif ($next === self::SPACE){
|
||||
$next = $iter->move();
|
||||
}
|
||||
|
||||
if ($next !== self::RIGHT_SQUARE_BRACKET){
|
||||
return null;
|
||||
}
|
||||
|
||||
++$this->checkbox_count;
|
||||
|
||||
$rest = trim($this->restToString($iter));
|
||||
$checked_attr = $checked ? ' checked' : '';
|
||||
|
||||
if ($checked){
|
||||
$rest = '<del>'.$rest.'</del>';
|
||||
}
|
||||
|
||||
if ($this->checkbox_name === null){
|
||||
return <<<HTML
|
||||
<div class="field-group">
|
||||
<input type="checkbox"$checked_attr disabled>
|
||||
<label class="disabled">$rest</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
else{
|
||||
$id = $this->checkbox_name.'-'.$this->checkbox_count;
|
||||
|
||||
return <<<HTML
|
||||
<div class="field-group">
|
||||
<input id="$id" name="$this->checkbox_name[]" type="checkbox" value="$this->checkbox_count"$checked_attr>
|
||||
<label for="$id">$rest</label>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
private function restToString(UnicodeIterator $iter): string{
|
||||
$str = '';
|
||||
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
||||
namespace Pages\Controllers\Tracker;
|
||||
|
||||
use Database\Objects\TrackerInfo;
|
||||
use Pages\Components\Forms\FormComponent;
|
||||
use Pages\Controllers\AbstractTrackerController;
|
||||
use Pages\IAction;
|
||||
use Pages\Models\BasicTrackerPageModel;
|
||||
@ -13,6 +14,7 @@ use Pages\Views\ErrorPage;
|
||||
use Pages\Views\Tracker\IssueDetailPage;
|
||||
use Routing\Request;
|
||||
use Session\Session;
|
||||
use function Pages\Actions\reload;
|
||||
use function Pages\Actions\view;
|
||||
|
||||
class IssueDetailController extends AbstractTrackerController{
|
||||
@ -25,8 +27,19 @@ class IssueDetailController extends AbstractTrackerController{
|
||||
|
||||
return view(new ErrorPage($error_model->load()));
|
||||
}
|
||||
|
||||
|
||||
$model = new IssueDetailModel($req, $tracker, $sess->getPermissions(), (int)$issue_id);
|
||||
$data = $req->getData();
|
||||
|
||||
if (!empty($data)){
|
||||
$action = $data[FormComponent::ACTION_KEY] ?? '';
|
||||
|
||||
if ($action === $model::ACTION_UPDATE_TASKS){
|
||||
$model->updateCheckboxes($data);
|
||||
return reload();
|
||||
}
|
||||
}
|
||||
|
||||
return view(new IssueDetailPage($model->load()));
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use Database\DB;
|
||||
use Database\Objects\IssueDetail;
|
||||
use Database\Objects\TrackerInfo;
|
||||
use Database\Tables\IssueTable;
|
||||
use Pages\Components\Markdown\MarkdownParseResult;
|
||||
use Pages\Components\Sidemenu\SidemenuComponent;
|
||||
use Pages\Components\Text;
|
||||
use Pages\IModel;
|
||||
@ -16,11 +17,15 @@ use Session\Permissions;
|
||||
use Session\Session;
|
||||
|
||||
class IssueDetailModel extends BasicTrackerPageModel{
|
||||
public const ACTION_UPDATE_TASKS = 'Update';
|
||||
public const CHECKBOX_NAME = 'Tasks';
|
||||
|
||||
private ?IssueDetail $issue = null;
|
||||
private int $issue_id;
|
||||
private bool $can_edit;
|
||||
|
||||
private Permissions $perms;
|
||||
private MarkdownParseResult $description;
|
||||
private SidemenuComponent $menu_actions;
|
||||
|
||||
public function __construct(Request $req, TrackerInfo $tracker, Permissions $perms, int $issue_id){
|
||||
@ -66,11 +71,23 @@ class IssueDetailModel extends BasicTrackerPageModel{
|
||||
if ($this->perms->checkTracker($tracker, IssuesModel::PERM_DELETE_ALL)){
|
||||
$this->menu_actions->addLink(Text::withIcon('Delete Issue', 'trash'), '/issues/'.$this->issue_id.'/delete');
|
||||
}
|
||||
|
||||
$desc = $issue->getDescription();
|
||||
|
||||
if ($this->can_edit){
|
||||
$desc->setCheckboxNameForEditing(self::CHECKBOX_NAME);
|
||||
}
|
||||
|
||||
$this->description = $desc->parse();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function canEditCheckboxes(): bool{
|
||||
return $this->can_edit;
|
||||
}
|
||||
|
||||
public function getIssue(): ?IssueDetail{
|
||||
return $this->issue;
|
||||
}
|
||||
@ -79,9 +96,29 @@ class IssueDetailModel extends BasicTrackerPageModel{
|
||||
return $this->issue_id;
|
||||
}
|
||||
|
||||
public function getDescription(): MarkdownParseResult{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getMenuActions(): SidemenuComponent{
|
||||
return $this->menu_actions;
|
||||
}
|
||||
|
||||
public function updateCheckboxes(array $data): void{
|
||||
$issues = new IssueTable(DB::get(), $this->getTracker());
|
||||
$description = $issues->getIssueDescription($this->issue_id);
|
||||
|
||||
$checked_indices = array_map(fn($i): int => intval($i), $data[self::CHECKBOX_NAME] ?? []);
|
||||
$index = 0;
|
||||
|
||||
$description = preg_replace_callback('/^\[[ xX]?]/mu', function(array $matches) use($checked_indices, &$index): string{
|
||||
return in_array(++$index, $checked_indices) ? '[x]' : '[ ]';
|
||||
}, $description);
|
||||
|
||||
if ($index > 0){
|
||||
$issues->updateIssueTasks($this->issue_id, $description, (int)floor(100.0 * count($checked_indices) / $index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types = 1);
|
||||
namespace Pages\Views\Tracker;
|
||||
|
||||
use Pages\Components\DateTimeComponent;
|
||||
use Pages\Components\Forms\FormComponent;
|
||||
use Pages\Components\ProgressBarComponent;
|
||||
use Pages\Components\Text;
|
||||
use Pages\IViewable;
|
||||
@ -46,9 +47,10 @@ class IssueDetailPage extends AbstractTrackerIssuePage{
|
||||
echo <<<HTML
|
||||
<div class="split-wrapper">
|
||||
<div class="split-80">
|
||||
<h3>Details</h3>
|
||||
<article>
|
||||
<div class="issue-details">
|
||||
<form action="" method="post">
|
||||
<h3>Details</h3>
|
||||
<article>
|
||||
<div class="issue-details">
|
||||
HTML;
|
||||
|
||||
$milestone = $issue->getMilestoneTitle();
|
||||
@ -71,29 +73,48 @@ HTML;
|
||||
/** @var IViewable $component */
|
||||
foreach($components as $title => $component){
|
||||
echo <<<HTML
|
||||
<div data-title="$title">
|
||||
<h4>$title</h4>
|
||||
<div data-title="$title">
|
||||
<h4>$title</h4>
|
||||
HTML;
|
||||
|
||||
$component->echoBody();
|
||||
|
||||
echo <<<HTML
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
echo <<<HTML
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h3>Description</h3>
|
||||
<article class="issue-description">
|
||||
<h3>Description</h3>
|
||||
<article class="issue-description">
|
||||
HTML;
|
||||
|
||||
$issue->getDescription()->echoBody();
|
||||
$description = $this->model->getDescription();
|
||||
$description->echoBody();
|
||||
|
||||
echo <<<HTML
|
||||
</article>
|
||||
</article>
|
||||
HTML;
|
||||
|
||||
if ($this->model->canEditCheckboxes() && $description->hasCheckboxes()){
|
||||
// TODO hide in JS
|
||||
echo <<<HTML
|
||||
<h3 data-task-submit>Tasks</h3>
|
||||
<article data-task-submit>
|
||||
<button class="styled" type="submit"><span class="icon icon-checkmark"></span> Update Tasks</button>
|
||||
</article>
|
||||
HTML;
|
||||
}
|
||||
|
||||
$action_key = FormComponent::ACTION_KEY;
|
||||
$action_value = IssueDetailModel::ACTION_UPDATE_TASKS;
|
||||
|
||||
echo <<<HTML
|
||||
<input type="hidden" name="$action_key" value="$action_value">
|
||||
</form>
|
||||
</div>
|
||||
<div class="split-20 min-width-250">
|
||||
HTML;
|
||||
|
Loading…
Reference in New Issue
Block a user