Release 2.57 (#5929)

This commit is contained in:
Kevin Papst
2026-05-21 22:14:10 +02:00
committed by GitHub
parent f0ce1c7bd6
commit 976d38e8a4
101 changed files with 4226 additions and 1119 deletions

View File

@@ -191,7 +191,7 @@ final class ActivityController extends BaseApiController
* Delete activity
*
* [DANGER] This will also delete ALL linked timesheets.
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}` instead?
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}`?
*/
#[IsGranted('delete', 'activity')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one activity')])]

View File

@@ -11,8 +11,10 @@ namespace App\API;
use App\Customer\CustomerService;
use App\Entity\Customer;
use App\Entity\CustomerComment;
use App\Entity\CustomerRate;
use App\Entity\User;
use App\Form\API\CommentApiForm;
use App\Form\API\CustomerApiEditForm;
use App\Form\API\CustomerRateApiForm;
use App\Repository\CustomerRateRepository;
@@ -35,6 +37,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Customer')]
final class CustomerController extends BaseApiController
{
private const GROUPS_COMMENT = ['Default', 'Not_Expanded'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Customer', 'Customer_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Customer'];
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
@@ -182,7 +185,7 @@ final class CustomerController extends BaseApiController
* Delete customer
*
* [DANGER] This will also delete ALL linked projects, project activities and timesheets.
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}` instead?
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}`?
*/
#[IsGranted('delete', 'customer')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one customer')])]
@@ -299,4 +302,103 @@ final class CustomerController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Fetch comments for customer
*/
#[IsGranted('view', 'customer')]
#[IsGranted('comments', 'customer')]
#[OA\Response(response: 200, description: 'Returns a collection of customer comments', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/Comment')))]
#[OA\Parameter(name: 'id', description: 'The customer whose comments will be returned', in: 'path', required: true)]
#[Route(path: '/{id}/comments', name: 'get_customer_comments', requirements: ['id' => '\d+'], methods: ['GET'])]
public function getCommentsAction(#[MapEntity(mapping: ['id' => 'id'])] Customer $customer): Response
{
$comments = $this->repository->getComments($customer);
$view = new View($comments, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Add comment for customer
*/
#[IsGranted('view', 'customer')]
#[IsGranted('comments', 'customer')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the newly created customer comment', content: new OA\JsonContent(ref: '#/components/schemas/Comment'))])]
#[OA\Parameter(name: 'id', description: 'The customer to add the comment for', in: 'path', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CommentForm'))]
#[Route(path: '/{id}/comments', name: 'post_customer_comment', requirements: ['id' => '\d+'], methods: ['POST'])]
public function postCommentAction(#[MapEntity(mapping: ['id' => 'id'])] Customer $customer, Request $request): Response
{
$comment = new CustomerComment($customer);
$comment->setCreatedBy($this->getUser());
$form = $this->createForm(CommentApiForm::class, $comment, [
'method' => 'POST',
]);
$form->setData($comment);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
return $this->viewHandler->handle(new View($form, Response::HTTP_BAD_REQUEST));
}
$this->repository->saveComment($comment);
$view = new View($comment, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Pin customer comment
*
* This toggles the `pinned` status of the given comment.
*/
#[IsGranted('view', 'customer')]
#[IsGranted('edit', 'customer')]
#[IsGranted('comments', 'customer')]
#[OA\Patch(responses: [new OA\Response(response: 200, description: 'Returns the updated customer comment', content: new OA\JsonContent(ref: '#/components/schemas/Comment'))])]
#[OA\Parameter(name: 'id', description: 'The customer whose comment will be pinned or unpinned', in: 'path', required: true)]
#[OA\Parameter(name: 'comment', description: 'The comment whose pinned status will be toggled', in: 'path', required: true)]
#[Route(path: '/{id}/comments/{comment}/pin', name: 'toggle_customer_comment_pin', requirements: ['id' => '\d+', 'comment' => '\d+'], methods: ['PATCH'])]
public function toggleCommentPin(#[MapEntity(mapping: ['id' => 'id'])] Customer $customer, #[MapEntity(mapping: ['comment' => 'id'])] CustomerComment $comment): Response
{
if ($comment->getCustomer() !== $customer) {
throw $this->createAccessDeniedException(\sprintf('Comment %s does not belong to customer %s', $comment->getId(), $customer->getId()));
}
$comment->setPinned(!$comment->isPinned());
$this->repository->saveComment($comment);
$view = new View($comment, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Delete customer comment
*/
#[IsGranted('view', 'customer')]
#[IsGranted('edit', 'customer')]
#[IsGranted('comments', 'customer')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
#[OA\Parameter(name: 'id', description: 'The customer whose comment will be removed', in: 'path', required: true)]
#[OA\Parameter(name: 'comment', description: 'The comment to remove', in: 'path', required: true)]
#[Route(path: '/{id}/comments/{comment}', name: 'delete_customer_comment', requirements: ['id' => '\d+', 'comment' => '\d+'], methods: ['DELETE'])]
public function deleteCommentAction(#[MapEntity(mapping: ['id' => 'id'])] Customer $customer, #[MapEntity(mapping: ['comment' => 'id'])] CustomerComment $comment): Response
{
if ($comment->getCustomer() !== $customer) {
throw $this->createAccessDeniedException(\sprintf('Comment %s does not belong to customer %s', $comment->getId(), $customer->getId()));
}
$this->repository->deleteComment($comment);
return $this->viewHandler->handle(new View(null, Response::HTTP_NO_CONTENT));
}
}

View File

@@ -10,8 +10,10 @@
namespace App\API;
use App\Entity\Project;
use App\Entity\ProjectComment;
use App\Entity\ProjectRate;
use App\Entity\User;
use App\Form\API\CommentApiForm;
use App\Form\API\ProjectApiEditForm;
use App\Form\API\ProjectRateApiForm;
use App\Project\ProjectService;
@@ -37,6 +39,7 @@ use Symfony\Component\Validator\Constraints;
#[OA\Tag(name: 'Project')]
final class ProjectController extends BaseApiController
{
private const GROUPS_COMMENT = ['Default', 'Not_Expanded'];
public const GROUPS_ENTITY = ['Default', 'Entity', 'Project', 'Project_Entity'];
public const GROUPS_COLLECTION = ['Default', 'Collection', 'Project'];
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
@@ -238,7 +241,7 @@ final class ProjectController extends BaseApiController
* Delete project
*
* [DANGER] This will also delete ALL linked activities and timesheets.
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}` instead?
* Do you want to use `PATCH` instead and mark it as inactive with `{visible: false}`?
*/
#[IsGranted('delete', 'project')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete one project')])]
@@ -355,4 +358,103 @@ final class ProjectController extends BaseApiController
return $this->viewHandler->handle($view);
}
/**
* Fetch comments for project
*/
#[IsGranted('view', 'project')]
#[IsGranted('comments', 'project')]
#[OA\Response(response: 200, description: 'Returns a collection of project comments', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/Comment')))]
#[OA\Parameter(name: 'id', description: 'The project whose comments will be returned', in: 'path', required: true)]
#[Route(path: '/{id}/comments', name: 'get_project_comments', requirements: ['id' => '\d+'], methods: ['GET'])]
public function getCommentsAction(#[MapEntity(mapping: ['id' => 'id'])] Project $project): Response
{
$comments = $this->repository->getComments($project);
$view = new View($comments, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Add comment for project
*/
#[IsGranted('view', 'project')]
#[IsGranted('comments', 'project')]
#[OA\Post(responses: [new OA\Response(response: 200, description: 'Returns the newly created project comment', content: new OA\JsonContent(ref: '#/components/schemas/Comment'))])]
#[OA\Parameter(name: 'id', description: 'The project to add the comment for', in: 'path', required: true)]
#[OA\RequestBody(required: true, content: new OA\JsonContent(ref: '#/components/schemas/CommentForm'))]
#[Route(path: '/{id}/comments', name: 'post_project_comment', requirements: ['id' => '\d+'], methods: ['POST'])]
public function postCommentAction(#[MapEntity(mapping: ['id' => 'id'])] Project $project, Request $request): Response
{
$comment = new ProjectComment($project);
$comment->setCreatedBy($this->getUser());
$form = $this->createForm(CommentApiForm::class, $comment, [
'method' => 'POST',
]);
$form->setData($comment);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) {
return $this->viewHandler->handle(new View($form, Response::HTTP_BAD_REQUEST));
}
$this->repository->saveComment($comment);
$view = new View($comment, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Pin project comment
*
* This toggles the `pinned` status of the given comment.
*/
#[IsGranted('view', 'project')]
#[IsGranted('edit', 'project')]
#[IsGranted('comments', 'project')]
#[OA\Patch(responses: [new OA\Response(response: 200, description: 'Returns the updated project comment', content: new OA\JsonContent(ref: '#/components/schemas/Comment'))])]
#[OA\Parameter(name: 'id', description: 'The project whose comment will be pinned or unpinned', in: 'path', required: true)]
#[OA\Parameter(name: 'comment', description: 'The comment whose pinned status will be toggled', in: 'path', required: true)]
#[Route(path: '/{id}/comments/{comment}/pin', name: 'toggle_project_comment_pin', requirements: ['id' => '\d+', 'comment' => '\d+'], methods: ['PATCH'])]
public function toggleCommentPin(#[MapEntity(mapping: ['id' => 'id'])] Project $project, #[MapEntity(mapping: ['comment' => 'id'])] ProjectComment $comment): Response
{
if ($comment->getProject() !== $project) {
throw $this->createAccessDeniedException(\sprintf('Comment %s does not belong to project %s', $comment->getId(), $project->getId()));
}
$comment->setPinned(!$comment->isPinned());
$this->repository->saveComment($comment);
$view = new View($comment, 200);
$view->getContext()->setGroups(self::GROUPS_COMMENT);
return $this->viewHandler->handle($view);
}
/**
* Delete project comment
*/
#[IsGranted('view', 'project')]
#[IsGranted('edit', 'project')]
#[IsGranted('comments', 'project')]
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Returns no content: 204 on successful delete')])]
#[OA\Parameter(name: 'id', description: 'The project whose comment will be removed', in: 'path', required: true)]
#[OA\Parameter(name: 'comment', description: 'The comment to remove', in: 'path', required: true)]
#[Route(path: '/{id}/comments/{comment}', name: 'delete_project_comment', requirements: ['id' => '\d+', 'comment' => '\d+'], methods: ['DELETE'])]
public function deleteCommentAction(#[MapEntity(mapping: ['id' => 'id'])] Project $project, #[MapEntity(mapping: ['comment' => 'id'])] ProjectComment $comment): Response
{
if ($comment->getProject() !== $project) {
throw $this->createAccessDeniedException(\sprintf('Comment %s does not belong to project %s', $comment->getId(), $project->getId()));
}
$this->repository->deleteComment($comment);
return $this->viewHandler->handle(new View(null, Response::HTTP_NO_CONTENT));
}
}

View File

@@ -113,6 +113,9 @@ final class TimesheetController extends BaseApiController
if (!$seeAll) {
foreach ($userRepository->findByIds($users) as $user) {
if (!$this->isGranted('access_user', $user)) {
throw $this->createAccessDeniedException('Cannot access user: ' . $user->getId());
}
$query->addUser($user);
}
}