1
0
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:
chylex 2020-08-07 19:08:07 +02:00
parent 50a04f80e6
commit 615623b9c0
8 changed files with 245 additions and 33 deletions
res/~resources/css
src

View File

@ -48,3 +48,7 @@
margin: 20px 0;
font-size: 18px;
}
.issue-description input[type=checkbox] {
margin-left: 1px;
}

View File

@ -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);

View File

@ -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();
}
}

View 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;
}
}
?>

View File

@ -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 = '';

View File

@ -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()));
}
}

View File

@ -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));
}
}
}
?>

View File

@ -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;