Merge branch 'develop' into feat/mail-integration

This commit is contained in:
2026-05-20 07:45:09 +00:00
53 changed files with 5666 additions and 506 deletions

View File

@@ -10,9 +10,12 @@ use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\ApiResource\SwitchWorkflowOutput;
use App\Repository\ProjectRepository;
use App\State\SwitchProjectWorkflowProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
@@ -30,6 +33,19 @@ use Symfony\Component\Validator\Constraints as Assert;
),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')"),
new Post(
uriTemplate: '/projects/{id}/switch-workflow',
uriVariables: ['id' => new Link(fromClass: Project::class)],
security: "is_granted('ROLE_ADMIN')",
input: false,
output: SwitchWorkflowOutput::class,
normalizationContext: ['groups' => ['switch_workflow:read']],
processor: SwitchProjectWorkflowProcessor::class,
read: true,
deserialize: false,
validate: false,
name: 'switch_workflow',
),
],
normalizationContext: ['groups' => ['project:read']],
denormalizationContext: ['groups' => ['project:write']],
@@ -69,6 +85,12 @@ class Project
#[Groups(['project:read', 'project:write'])]
private ?Client $client = null;
#[ORM\ManyToOne(targetEntity: Workflow::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'RESTRICT')]
#[Groups(['project:read', 'project:write', 'task:read'])]
#[Assert\NotNull(message: 'Un projet doit avoir un workflow.')]
private ?Workflow $workflow = null;
#[ORM\Column(length: 255, nullable: true)]
#[Groups(['project:read', 'project:write', 'task:read'])]
private ?string $giteaOwner = null;
@@ -228,6 +250,18 @@ class Project
return $this;
}
public function getWorkflow(): ?Workflow
{
return $this->workflow;
}
public function setWorkflow(Workflow $workflow): static
{
$this->workflow = $workflow;
return $this;
}
#[Groups(['project:read'])]
public function getTaskCount(): int
{

View File

@@ -478,4 +478,26 @@ class Task
;
}
}
#[Assert\Callback]
public function validateStatusBelongsToProjectWorkflow(ExecutionContextInterface $context): void
{
if (null === $this->status || null === $this->project) {
return;
}
$projectWorkflow = $this->project->getWorkflow();
$statusWorkflow = $this->status->getWorkflow();
if (null === $projectWorkflow || null === $statusWorkflow) {
return;
}
if ($projectWorkflow->getId() !== $statusWorkflow->getId()) {
$context->buildViolation('Status does not belong to this project\'s workflow.')
->atPath('status')
->addViolation()
;
}
}
}

View File

@@ -10,9 +10,11 @@ use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Enum\StatusCategory;
use App\Repository\TaskStatusRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
@@ -32,25 +34,36 @@ class TaskStatus
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['task_status:read', 'task:read'])]
#[Groups(['task_status:read', 'task:read', 'workflow:read', 'project:read'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?string $label = null;
#[ORM\Column(length: 7)]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?string $color = '#222783';
#[ORM\Column]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private ?int $position = 0;
#[ORM\Column(type: 'boolean')]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
private bool $isFinal = false;
#[ORM\ManyToOne(targetEntity: Workflow::class, inversedBy: 'statuses')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
#[Groups(['task_status:read', 'task_status:write', 'task:read'])]
#[Assert\NotNull]
private ?Workflow $workflow = null;
#[ORM\Column(type: 'string', length: 32, enumType: StatusCategory::class)]
#[Groups(['task_status:read', 'task_status:write', 'task:read', 'workflow:read', 'project:read'])]
#[Assert\NotNull]
private ?StatusCategory $category = null;
public function getId(): ?int
{
return $this->id;
@@ -103,4 +116,28 @@ class TaskStatus
return $this;
}
public function getWorkflow(): ?Workflow
{
return $this->workflow;
}
public function setWorkflow(?Workflow $workflow): static
{
$this->workflow = $workflow;
return $this;
}
public function getCategory(): ?StatusCategory
{
return $this->category;
}
public function setCategory(StatusCategory $category): static
{
$this->category = $category;
return $this;
}
}

131
src/Entity/Workflow.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use App\Repository\WorkflowRepository;
use App\State\WorkflowDeleteProcessor;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
operations: [
new GetCollection(security: "is_granted('ROLE_USER')"),
new Get(security: "is_granted('ROLE_USER')"),
new Post(security: "is_granted('ROLE_ADMIN')"),
new Patch(security: "is_granted('ROLE_ADMIN')"),
new Delete(security: "is_granted('ROLE_ADMIN')", processor: WorkflowDeleteProcessor::class),
],
normalizationContext: ['groups' => ['workflow:read']],
denormalizationContext: ['groups' => ['workflow:write']],
order: ['position' => 'ASC'],
)]
#[ORM\Entity(repositoryClass: WorkflowRepository::class)]
#[UniqueEntity(fields: ['name'], message: 'Ce nom de workflow est déjà utilisé.')]
class Workflow
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['workflow:read', 'project:read', 'task_status:read'])]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['workflow:read', 'workflow:write', 'project:read'])]
#[Assert\NotBlank]
private ?string $name = null;
#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['workflow:read', 'workflow:write'])]
private bool $isDefault = false;
#[ORM\Column(type: 'integer', options: ['default' => 0])]
#[Groups(['workflow:read', 'workflow:write'])]
private int $position = 0;
/** @var Collection<int, TaskStatus> */
#[ORM\OneToMany(targetEntity: TaskStatus::class, mappedBy: 'workflow', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['position' => 'ASC'])]
#[Groups(['workflow:read', 'project:read'])]
private Collection $statuses;
public function __construct()
{
$this->statuses = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function setIsDefault(bool $isDefault): static
{
$this->isDefault = $isDefault;
return $this;
}
public function getPosition(): int
{
return $this->position;
}
public function setPosition(int $position): static
{
$this->position = $position;
return $this;
}
/** @return Collection<int, TaskStatus> */
public function getStatuses(): Collection
{
return $this->statuses;
}
public function addStatus(TaskStatus $status): static
{
if (!$this->statuses->contains($status)) {
$this->statuses->add($status);
$status->setWorkflow($this);
}
return $this;
}
public function removeStatus(TaskStatus $status): static
{
$this->statuses->removeElement($status);
return $this;
}
}