Implement snip hiding

This commit is contained in:
Tim 2025-04-25 22:17:27 +02:00
parent 5a940b9ebd
commit 7c4a2b46c0
13 changed files with 541 additions and 24 deletions

View File

@ -12,14 +12,19 @@
"doctrine/orm": "^2.14",
"league/commonmark": "^2.6",
"league/pipeline": "^1.0",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1",
"symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*",
"symfony/flex": "^2",
"symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*",
"symfony/monolog-bundle": "^3.0",
"symfony/property-access": "7.2.*",
"symfony/property-info": "7.2.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/serializer": "7.2.*",
"symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*",
"symfony/validator": "7.2.*",

380
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "75aeaffb0d55910fec640906e3460861",
"content-hash": "ea24d0c80afdb2f4436ce6907ae3e570",
"packages": [
{
"name": "dflydev/dot-access-data",
@ -1899,6 +1899,228 @@
},
"time": "2025-03-30T21:06:30+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "5.6.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "92dde6a5919e34835c506ac8c523ef095a95ed62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62",
"reference": "92dde6a5919e34835c506ac8c523ef095a95ed62",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.1",
"ext-filter": "*",
"php": "^7.4 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.7",
"phpstan/phpdoc-parser": "^1.7|^2.0",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
"mockery/mockery": "~1.3.5 || ~1.6.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-mockery": "^1.1",
"phpstan/phpstan-webmozart-assert": "^1.2",
"phpunit/phpunit": "^9.5",
"psalm/phar": "^5.26"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
},
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2"
},
"time": "2025-04-13T19:20:35+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a",
"reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a",
"shasum": ""
},
"require": {
"doctrine/deprecations": "^1.0",
"php": "^7.3 || ^8.0",
"phpdocumentor/reflection-common": "^2.0",
"phpstan/phpdoc-parser": "^1.18|^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"phpbench/phpbench": "^1.2",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^9.5",
"rector/rector": "^0.13.9",
"vimeo/psalm": "^4.25"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0"
},
"time": "2024-11-09T15:12:26+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "2.1.0",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"require-dev": {
"doctrine/annotations": "^2.0",
"nikic/php-parser": "^5.3.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^9.6",
"symfony/process": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPStan\\PhpDocParser\\": [
"src/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0"
},
"time": "2025-02-19T13:28:12+00:00"
},
{
"name": "psr/cache",
"version": "3.0.0",
@ -5287,6 +5509,104 @@
],
"time": "2025-02-11T16:46:20+00:00"
},
{
"name": "symfony/serializer",
"version": "v7.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
"reference": "d8b75b2c8144c29ac43b235738411f7cca6d584d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer/zipball/d8b75b2c8144c29ac43b235738411f7cca6d584d",
"reference": "d8b75b2c8144c29ac43b235738411f7cca6d584d",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/dependency-injection": "<6.4",
"symfony/property-access": "<6.4",
"symfony/property-info": "<6.4",
"symfony/uid": "<6.4",
"symfony/validator": "<6.4",
"symfony/yaml": "<6.4"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
"phpstan/phpdoc-parser": "^1.0|^2.0",
"seld/jsonlint": "^1.10",
"symfony/cache": "^6.4|^7.0",
"symfony/config": "^6.4|^7.0",
"symfony/console": "^6.4|^7.0",
"symfony/dependency-injection": "^7.2",
"symfony/error-handler": "^6.4|^7.0",
"symfony/filesystem": "^6.4|^7.0",
"symfony/form": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/property-access": "^6.4|^7.0",
"symfony/property-info": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3",
"symfony/type-info": "^7.1",
"symfony/uid": "^6.4|^7.0",
"symfony/validator": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0",
"symfony/var-exporter": "^6.4|^7.0",
"symfony/yaml": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Serializer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/serializer/tree/v7.2.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-03-24T12:37:32+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.5.1",
@ -6420,6 +6740,64 @@
}
],
"time": "2025-02-13T08:34:43+00:00"
},
{
"name": "webmozart/assert",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
"reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"php": "^7.2 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.11.0"
},
"time": "2022-06-03T18:03:27+00:00"
}
],
"packages-dev": [

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250425182334 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE snip ADD visible TINYINT(1) NOT NULL
SQL);
$this->addSql(<<<'SQL'
UPDATE snip SET visible = 1
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE snip DROP visible
SQL);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Controller;
use App\Dto\SnipFilterRequest;
use App\Entity\Snip;
use App\Form\ConfirmationType;
use App\Form\SnipType;
@ -13,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryString;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/snip', name: 'snip')]
@ -24,20 +26,19 @@ class SnipController extends AbstractController
) {}
#[Route('/', name: '_index')]
public function index(): Response
public function index(#[MapQueryString] SnipFilterRequest $request): Response
{
return $this->render('snip/index.html.twig', [
'snips' => $this->repository->findByUser($this->getUser()),
'title' => 'My Snips',
'snips' => $this->repository->findByRequest($this->getUser(), $request),
'request' => $request,
]);
}
#[Route('/public', name: '_public')]
public function public(): Response
{
return $this->render('snip/index.html.twig', [
return $this->render('snip/public.html.twig', [
'snips' => $this->repository->findPublic(),
'title' => 'Public Snips',
]);
}
@ -86,7 +87,7 @@ class SnipController extends AbstractController
* It technically fully works, but rendering the version history needs an update first
*/
$isLatest = $snip->getActiveVersion() === $snip->getLatestVersion();
if(!$isLatest) {
if (!$isLatest) {
$this->addFlash('error', 'Snip is not the latest version, changes will not be saved.');
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Dto;
readonly class SnipFilterRequest
{
public function __construct(
public bool $onlyVisible = true,
) {}
public function toArray(): array
{
return [
'onlyVisible' => $this->onlyVisible,
];
}
}

View File

@ -33,6 +33,9 @@ class Snip
#[ORM\Column(length: 255)]
private ?string $parser = null;
#[ORM\Column]
private bool $visible = true;
public function __construct()
{
$this->snipContents = new ArrayCollection();
@ -130,4 +133,16 @@ class Snip
return $this;
}
public function isVisible(): ?bool
{
return $this->visible;
}
public function setVisible(bool $visible): static
{
$this->visible = $visible;
return $this;
}
}

View File

@ -28,7 +28,8 @@ class SnipType extends AbstractType
'attr' => ['rows' => 20],
'mapped' => false,
])
->add('public')
->add('public', SwitchType::class)
->add('visible', SwitchType::class)
;
}

22
src/Form/SwitchType.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class SwitchType extends AbstractType
{
public function getParent(): string
{
return CheckboxType::class;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['label_attr']['class'] = trim(($view->vars['label_attr']['class'] ?? '') . ' checkbox-switch');
$view->vars['required'] = false;
}
}

View File

@ -2,10 +2,12 @@
namespace App\Repository;
use App\Dto\SnipFilterRequest;
use App\Entity\Snip;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @extends ServiceEntityRepository<Snip>
@ -40,12 +42,17 @@ class SnipRepository extends ServiceEntityRepository
}
}
public function findByUser(User $user): array
public function findByRequest(UserInterface $user, SnipFilterRequest $request): array
{
$qb = $this->createQueryBuilder('s');
$qb->where('s.createdBy = :user')
->setParameter('user', $user)
->orderBy('s.createdAt', 'DESC')
$qb = $this
->createQueryBuilder('s')
->where('s.createdBy = :user')
->setParameter('user', $user)
->orderBy('s.createdAt', 'DESC')
;
$qb->andWhere('s.visible = :visible')
->setParameter('visible', $request->onlyVisible)
;
return $qb->getQuery()->getResult();
@ -53,10 +60,11 @@ class SnipRepository extends ServiceEntityRepository
public function findPublic(?User $user = null): array
{
$qb = $this->createQueryBuilder('s')
->where('s.public = :public')
->setParameter('public', true)
->orderBy('s.createdAt', 'DESC')
$qb = $this
->createQueryBuilder('s')
->where('s.public = true')
->andWhere('s.visible = true')
->orderBy('s.createdAt', 'DESC')
;
if ($user) {

View File

@ -1,3 +1,13 @@
{% if snip.visible %}
<span class="badge bg-success">
<i class="fa fa-eye"></i>
</span>
{% else %}
<span class="badge bg-secondary">
<i class="fa fa-eye-slash"></i>
</span>
{% endif %}
{% if snip.public %}
<span class="badge bg-info">
<i class="fa fa-lock-open"></i>

View File

@ -1,6 +1,10 @@
{% extends 'base/single.column.html.twig' %}
{% set title = 'Snip edit ' ~ snip %}
{% if snip.id %}
{% set title = 'Edit Snip ' ~ snip %}
{% else %}
{% set title = 'Create Snip' %}
{% endif %}
{% block body %}
{% if snip.id %}

View File

@ -1,22 +1,24 @@
{% extends 'base/single.column.html.twig' %}
{% set title = 'My Snips' %}
{% block body %}
<a class="btn btn-success" href="{{ path('snip_new') }}">
<i class="fa fa-plus"></i> Add
</a>
{% if request.onlyVisible %}
<a class="btn btn-secondary" href="{{ path('snip_index', {onlyVisible: false}) }}">Show hidden</a>
{% else %}
<a class="btn btn-secondary" href="{{ path('snip_index', {onlyVisible: true}) }}">Hide hidden</a>
{% endif %}
<br><br>
<div class="list-group">
{% for snip in snips %}
<a class="list-group-item d-flex justify-content-between" href="{{ path('snip_single', {snip: snip.id}) }}">
<span>
{% if snip.createdBy == app.user %}
{{ include('snip/badge.html.twig', {snip: snip}) }}
{% endif %}
{{ include('snip/badge.html.twig', {snip: snip}) }}
{{ snip }}
</span>
{% if snip.createdBy != app.user %}
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
{% endif %}
</a>
{% endfor %}
</div>

View File

@ -0,0 +1,16 @@
{% extends 'base/single.column.html.twig' %}
{% set title = 'Public Snips' %}
{% block body %}
<div class="list-group">
{% for snip in snips %}
<a class="list-group-item d-flex justify-content-between" href="{{ path('snip_single', {snip: snip.id}) }}">
<span>
{{ snip }}
</span>
{{ include('user/badge.html.twig', {user: snip.createdBy}) }}
</a>
{% endfor %}
</div>
{% endblock %}