Implement flowchart renderer for versions

This commit is contained in:
Tim 2025-05-14 00:22:25 +02:00
parent 6d20661305
commit 0e5d92258d
7 changed files with 142 additions and 32 deletions

View File

@ -81,15 +81,6 @@ class SnipController extends AbstractController
{
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
/**
* Temporary solution to prevent editing of old versions
* It technically fully works, but rendering the version history needs an update first
*/
$isLatest = $snip->getActiveVersion() === $snip->getLatestVersion();
if (!$isLatest) {
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
}
$form = $this->createForm(SnipType::class, $snip);
$form->add('Save', SubmitType::class);
if ($snip->getId()) {
@ -98,11 +89,6 @@ class SnipController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (!$isLatest) {
return $this->redirectToRoute('snip_single', [
'snip' => $snip->getId(),
]);
}
$this->repository->save($snip);
$contentService->update(
$snip,
@ -112,9 +98,7 @@ class SnipController extends AbstractController
$this->addFlash('success', sprintf('Snip "%s" saved', $snip));
return $this->redirectToRoute('snip_single', [
'snip' => $snip->getId(),
]);
return $this->redirectToRoute('snip_single', ['snip' => $snip->getId()]);
}
return $this->render('snip/edit.html.twig', [

View File

@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\Snip;
use App\Entity\SnipContent;
use App\Security\Voter\SnipVoter;
use App\Service\SnipContent\FlowChartTreeBuilder;
use App\Service\SnipContent\SnipContentService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@ -18,12 +19,16 @@ class VersionController extends AbstractController
) {}
#[Route('/', name: '_index')]
public function index(Snip $snip): Response
public function index(Snip $snip, FlowChartTreeBuilder $builder): Response
{
$this->denyAccessUnlessGranted(SnipVoter::EDIT, $snip);
$buildTree = $builder->buildTree($snip);
dump($buildTree);
return $this->render('version/index.html.twig', [
'snip' => $snip,
// 'versions' => new GitTreeBuilder($snip)->buildTree($snip->getSnipContents()->first()),
'versions' => $buildTree,
]);
}

View File

@ -43,6 +43,11 @@ class SnipContent
$this->children = new ArrayCollection();
}
public function __toString(): string
{
return $this->name ?? $this->id->toBase32();
}
public function getId(): ?Ulid
{
return $this->id;

View File

@ -6,7 +6,6 @@ use App\Entity\Snip;
use App\Service\SnipParser\ParserFactory;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
@ -36,6 +35,7 @@ class SnipType extends AbstractType
->add('contentName', TextType::class, [
'label' => 'Change description (optional)',
'mapped' => false,
'required' => false,
])
;
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Service\SnipContent;
use App\Entity\Snip;
use Symfony\Component\Routing\RouterInterface;
readonly class FlowChartTreeBuilder
{
public function __construct(
private RouterInterface $router,
) {}
public function buildTree(Snip $snip): array
{
$tree = [];
foreach ($snip->getSnipContents() as $content) {
if ($content->getParent()) {
$tree[] = sprintf('%s --> %s', $content->getParent(), $content);
}
}
foreach ($snip->getSnipContents() as $content) {
$tree[] = sprintf(
'click %s href "%s"',
$content,
$this->router->generate('version_set', ['snip' => $snip->getId(), 'version' => $content->getId()])
);
$tree[] = sprintf('%s@{ shape: rounded }', $content);
}
$tree[] = sprintf('class %s active', $snip->getActiveVersion());
return $tree;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Service\SnipContent;
use App\Entity\Snip;
use App\Entity\SnipContent;
class GitTreeBuilder
{
private string $activeBranch = 'main';
public function __construct(private readonly Snip $snip) {}
public function buildTree(SnipContent $content, string $branch = 'main'): array
{
$tree = [];
if ($this->activeBranch !== $branch) {
$tree[] = $this->checkout($branch);
}
$commit = sprintf('commit id:"%s"', $content);
if ($this->snip->getActiveVersion() === $content) {
$commit .= ' tag: "active" type: REVERSE';
}
$tree[] = $commit;
$first = true;
foreach ($content->getChildren() as $child) {
if (!$first) {
$tree[] = $this->branch($child);
$tree[] = $this->checkout($branch);
}
$first = false;
}
$first = true;
foreach ($content->getChildren() as $child) {
if ($first) {
$myBranch = $branch;
} else {
$tree[] = $this->checkout($child);
$myBranch = $child;
}
$tree = array_merge($tree, self::buildTree($child, $myBranch));
$first = false;
}
return $tree;
}
private function branch(string $branch): string
{
$this->activeBranch = $branch;
return sprintf('branch %s', $branch);
}
private function checkout(string $branch): string
{
$this->activeBranch = $branch;
return sprintf('checkout %s', $branch);
}
}

View File

@ -12,17 +12,35 @@
<a href="{{ path('content_compare', {to: snip.activeVersion.id}) }}" class="btn btn-info">
<i class="fa fa-left-right"></i> Compare
</a>
<br><br>
<div class="list-group">
{% for version in snip.snipContents|reverse %}
<a class="list-group-item {% if version.id == snip.activeVersion.id %}list-group-item-success{% endif %} d-flex justify-content-between"
href="{{ path('version_set', {version: version.id, snip: snip.id}) }}">
<span>
{{ include('generic/datetime.badge.html.twig', {datetime: version.id.dateTime}) }}
{% if version.name %}{{ version.name }}{% endif %}
</span>
<span class="text-muted">{{ version.id }}</span>
</a>
<pre class="mermaid">
flowchart BT
{% for versionData in versions %}
{{~ versionData ~}}
{% endfor %}
</div>
</pre>
{% endblock %}
{% block js %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({startOnLoad: true});
</script>
{% endblock %}
{% block css %}
{{ parent() }}
<style>
.node rect {
fill: var(--bs-secondary) !important;
stroke: var(--bs-secondary-text-emphasis) !important;
}
.node span {
color: var(--bs-light) !important;
}
.active rect {
fill: var(--bs-success) !important;
stroke: var(--bs-success-text-emphasis) !important;
}
</style>
{% endblock %}