diff --git a/CHANGELOG.md b/CHANGELOG.md index 388a60f..872c3a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ CHANGELOG =================== +## v2.5.0 - (2024-06-25) + +### Added +- `show_history_field.html.twig` based on tailwind class + Twig `HistoryExtension` required to autocomplete some data of the history rows +- `HistoryExtension` to automaticaly add the mentioned above template on the show view of every entity that implement the `Smart\CoreBundle\Entity\HistoryInterface` +- iconify cdn to use **iconify-icon Web Component** in `standard_layout.html.twig` and `empty_layout.html.twig ` +- Twig `FormatExtension` to detect data type base on string value +- `templates/macros/badge.html.twig` to use in tailwind block +- `AbstractAdmin::showHistoryTemplate` property to ease the override of the show_history_template.html.twig per admin + +### Changed +- **BC Break** `SendAccountCreationEmailTrait::sendAccountCreationEmailAction` the user subject is now entirely passed to the `BaseMailer` to log the email sent in +his history. + - You must add the `MailableInterface` to your User entity for `BaseMailer::setRecipientToEmail` to work properly +- `BaseMailer::setRecipientToEmail` advanced scenario to init the **to**, **cc** and **bcc** of the email based on the recipient type +- **BC Break** `AbstractApiCallAdmin` and `AbstractCronAdmin` now use **messages** for `choice_translation_domain` for their **type** properties + - You must move your cron.my_command.label translations on the **messages.%lang%.%format%** file instead of using the **admin.%lang%.%format%** +- `api_call_status.html.twig` now display null status code as "Ongoing" placeholder text +- `Parameter` entity now use `HistorizableInterface` from core-bundle + - Impact on `ParameterAdmin` : HistoryLogger DI removed + all previous log mention removed + - No more need to declare the `list_value.html.twig` and `timeline_history_field.html.twig` on the project as they are handle by the Sonata-Bundle + - **BC Break** database migration require to keep old history legacy : + ```php + addSql('ALTER TABLE smart_parameter CHANGE history history_legacy JSON DEFAULT NULL'); + $this->addSql('ALTER TABLE smart_parameter ADD history JSON DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE smart_parameter DROP history'); + $this->addSql('ALTER TABLE smart_parameter CHANGE history_legacy history JSON DEFAULT NULL'); + } + } + ``` + +### Flag as deprecated + +In anticipation for the v3.0.0 (cf. [UPGRADE-3.0.md](UPGRADE-3.0.md)) we mark the following classes as deprecated : + +- `Smart\SonataBundle\Entity\Log\BatchLog` +- `Smart\SonataBundle\Entity\Log\HistorizableInterface` +- `Smart\SonataBundle\Entity\Log\HistorizableTrait` +- `Smart\SonataBundle\Logger\BatchLogger` +- `Smart\SonataBundle\Logger\HistoryLogger` +- `templates\admin\base_field\timeline_history_field.html.twig` + ## v2.4.0 - (2024-06-12) ### Added - `DocumentationController::renderMarkdown` action to render markdown documentation files stored in the **/documentation** directory diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md new file mode 100644 index 0000000..c40c76b --- /dev/null +++ b/UPGRADE-3.0.md @@ -0,0 +1,25 @@ +UPGRADE FROM 2.X to 3.0 +=================== + +## BC Break + +This section list all the changes that are considered BC Break and will node code adjustment from upgrading to version 3 (not released yet). + +### Entity + +- Remove `Smart\SonataBundle\Entity\Log\BatchLog`, use `Smart\CoreBundle\Entity\ProcessTrait` and `Smart\CoreBundle\Entity\ProcessInterface` instead +- Remove `Smart\SonataBundle\Entity\Log\HistorizableInterface`, use `Smart\CoreBundle\Entity\Log\HistorizableInterface` instead +- Remove `Smart\SonataBundle\Entity\Log\HistorizableTrait`, use `Smart\CoreBundle\Entity\Log\HistorizableTrait` instead + +### Logger + +- Remove `Smart\SonataBundle\Logger\BatchLogger`, use `Smart\CoreBundle\Monitor\ProcessMonitor` instead +- Remove `Smart\SonataBundle\Logger\HistoryLogger`, use `Smart\CoreBundle\Logger\HistoryLogger` instead + +### Security + +- `SmartUserInterface` now extends `MailableInterface` so you need to define his methods + +### Templates + +- Remove `timeline_history_field.html.twig`, use `show_history_field.html.twig` instead diff --git a/assets/styles/_documentation.scss b/assets/styles/_documentation.scss index fd4bb20..1254e6e 100644 --- a/assets/styles/_documentation.scss +++ b/assets/styles/_documentation.scss @@ -37,4 +37,9 @@ @apply font-semibold; } } + p { + img { + @apply block; + } + } } diff --git a/assets/styles/_skin.scss b/assets/styles/_skin.scss index 6e094df..03c6a01 100644 --- a/assets/styles/_skin.scss +++ b/assets/styles/_skin.scss @@ -4,6 +4,16 @@ padding-top: 79px; } } + .content-header:has(.content-header-under-banner) { + margin-top: 29px; + } + .content-header-under-banner { + .sticky-wrapper { + .navbar.navbar-default.stuck { + margin-top: 29px; + } + } + } .main-sidebar { hr { margin-bottom: 0; diff --git a/assets/styles/_tailwind.scss b/assets/styles/_tailwind.scss index 3b771df..7b375f7 100644 --- a/assets/styles/_tailwind.scss +++ b/assets/styles/_tailwind.scss @@ -1,4 +1,7 @@ .sb-tailwind { + // Additional overhead to have an ISO rendering with our platform-bundle + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + .ps-0 { padding-inline-start: 0px; } @@ -15,4 +18,7 @@ .container-small { @apply mx-auto w-full max-w-screen-md; /* 768px */ } + .sb-history-comment { + @apply prose prose-sm prose-stone prose-a:text-info prose-a:no-underline hover:prose-a:underline max-w-none rounded-lg border border-solid border-neutral-lighter py-2 px-4 my-2; + } } diff --git a/composer.json b/composer.json index c20eaa4..0b4a05f 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "yokai/security-token-bundle": "^3.3", "sentry/sentry-symfony": "^4.1", "symfony/expression-language": "^4.4 || ^5.4 || ^6.0", - "smartbooster/core-bundle": "^1.6", + "smartbooster/core-bundle": "^1.8", "yokai/enum-bundle": "^4.1" }, "require-dev": { diff --git a/config/bundle_prepend_config.yml b/config/bundle_prepend_config.yml index 464a730..bc82be3 100644 --- a/config/bundle_prepend_config.yml +++ b/config/bundle_prepend_config.yml @@ -17,6 +17,9 @@ sonata_admin: admin.extension.encode_password: uses: - Smart\SonataBundle\Entity\User\UserTrait + admin.extension.history: + implements: + - Smart\CoreBundle\Entity\HistoryInterface # Global Timezone Config # https://stackoverflow.com/a/26469662 diff --git a/config/services.yaml b/config/services.yaml index 1f9b954..5612280 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -15,7 +15,6 @@ services: - Smart\SonataBundle\Entity\Parameter - ~ - '@Smart\SonataBundle\Enum\ParameterTypeEnum' - - '@Smart\SonataBundle\Logger\HistoryLogger' tags: - {name: sonata.admin, manager_type: orm, label: dashboard.label_parameter} @@ -62,6 +61,10 @@ services: - '@Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface' tags: - {name: sonata.admin.extension} + admin.extension.history: + class: Smart\SonataBundle\Admin\Extension\HistoryExtension + tags: + - {name: sonata.admin.extension, global: true} # Command smart_sonata.parameter_load_command: @@ -146,7 +149,7 @@ services: - '@Symfony\Component\Mailer\MailerInterface' - '@Smart\SonataBundle\Mailer\EmailProvider' - '@Symfony\Contracts\Translation\TranslatorInterface' - - '@Smart\SonataBundle\Logger\HistoryLogger' + - '@Smart\CoreBundle\Logger\HistoryLogger' Smart\SonataBundle\Mailer\EmailProvider: arguments: - '@request_stack' @@ -176,6 +179,7 @@ services: arguments: - '@security.processor.last_login' - '@translator' + - '@Smart\CoreBundle\EventListener\HistoryDoctrineListener' tags: - { name: kernel.event_subscriber } Smart\SonataBundle\Security\Handler\SmartSecurityHandler: @@ -185,3 +189,11 @@ services: # Templating Sonata\AdminBundle\Templating\TemplateRegistry: alias: 'sonata.admin.global_template_registry' + + # Twig + Smart\SonataBundle\Twig\Extension\FormatExtension: + tags: [ 'twig.extension' ] + Smart\SonataBundle\Twig\Extension\HistoryExtension: + calls: + - setTranslator: [ '@translator' ] + tags: ['twig.extension'] diff --git a/src/Admin/AbstractAdmin.php b/src/Admin/AbstractAdmin.php index 26a367f..4f5e5f9 100644 --- a/src/Admin/AbstractAdmin.php +++ b/src/Admin/AbstractAdmin.php @@ -23,6 +23,7 @@ abstract class AbstractAdmin extends \Sonata\AdminBundle\Admin\AbstractAdmin imp /** @var ContainerInterface $container */ private $container; private TokenStorageInterface $tokenStorage; + public ?string $showHistoryTemplate = null; public function __construct(?string $code = null, ?string $class = null, ?string $baseControllerName = null) { diff --git a/src/Admin/Extension/HistoryExtension.php b/src/Admin/Extension/HistoryExtension.php new file mode 100644 index 0000000..5ac156a --- /dev/null +++ b/src/Admin/Extension/HistoryExtension.php @@ -0,0 +1,38 @@ + + */ +class HistoryExtension extends AbstractAdminExtension +{ + public function configureShowFields(ShowMapper $show): void + { + $admin = $show->getAdmin(); + $subject = $admin->getSubject(); + if (!$subject instanceof HistorizableInterface) { + return; + } + + $tabs = $admin->getShowTabs(); + if (count($tabs) === 1 && $tabs[array_key_first($tabs)]['auto_created']) { + $show->end(); + } + $groups = $admin->getShowGroups(); + if (!isset($tabs['history_tab'])) { + $show->tab('history_tab', ['label' => 'label.history']); + if (!isset($groups['history_group'])) { + $show->with('history_group', ['label' => 'label.history']); + } + } + + if (!$show->has('history')) { + $show->add('history', null, ['template' => $admin->showHistoryTemplate ?? '@SmartSonata/admin/base_field/show_history_field.html.twig']); + } + } +} diff --git a/src/Admin/Monitoring/AbstractApiCallAdmin.php b/src/Admin/Monitoring/AbstractApiCallAdmin.php index d4ea3d6..d2a7af5 100644 --- a/src/Admin/Monitoring/AbstractApiCallAdmin.php +++ b/src/Admin/Monitoring/AbstractApiCallAdmin.php @@ -118,6 +118,7 @@ protected function configureShowFields(ShowMapper $show): void ->add('type', FieldDescriptionInterface::TYPE_CHOICE, [ 'label' => 'label.route', 'choices' => array_flip($this->getRouteChoices()), + 'choice_translation_domain' => 'messages', ]) ->add('startedAt', null, ['label' => 'label.started_at']) ->add('endedAt', null, ['label' => 'label.ended_at']) diff --git a/src/Admin/Monitoring/AbstractCronAdmin.php b/src/Admin/Monitoring/AbstractCronAdmin.php index 6b71b81..d12fa0f 100644 --- a/src/Admin/Monitoring/AbstractCronAdmin.php +++ b/src/Admin/Monitoring/AbstractCronAdmin.php @@ -46,7 +46,7 @@ protected function configureDatagridFilters(DatagridMapper $filter): void 'field_type' => ChoiceType::class, 'field_options' => [ 'choices' => $this->commandPoolHelper->getCronChoices(), - 'choice_translation_domain' => 'admin', + 'choice_translation_domain' => 'messages', ], ]) ->add('status', ChoiceFilter::class, [ @@ -77,7 +77,7 @@ protected function configureListFields(ListMapper $list): void ->addIdentifier('type', FieldDescriptionInterface::TYPE_CHOICE, [ 'label' => 'label.type', 'choices' => array_flip($this->commandPoolHelper->getCronChoices()), - 'choice_translation_domain' => 'admin', + 'choice_translation_domain' => 'messages', 'sortable' => false, ]) ->add('startedAt', null, ['label' => 'label.started_at']) @@ -101,7 +101,7 @@ protected function configureShowFields(ShowMapper $show): void ->add('type', FieldDescriptionInterface::TYPE_CHOICE, [ 'label' => 'label.type', 'choices' => array_flip($this->commandPoolHelper->getCronChoices()), - 'choice_translation_domain' => 'admin', + 'choice_translation_domain' => 'messages', ]) ->add('startedAt', null, ['label' => 'label.started_at']) ->add('endedAt', null, ['label' => 'label.ended_at']) diff --git a/src/Admin/ParameterAdmin.php b/src/Admin/ParameterAdmin.php index 38bd919..ca948f3 100644 --- a/src/Admin/ParameterAdmin.php +++ b/src/Admin/ParameterAdmin.php @@ -2,12 +2,9 @@ namespace Smart\SonataBundle\Admin; -use Doctrine\ORM\UnitOfWork; use Smart\CoreBundle\Validator\Constraints\EmailChain; -use Smart\SonataBundle\Entity\Log\HistorizableInterface; use Smart\SonataBundle\Entity\ParameterInterface; use Smart\SonataBundle\Enum\ParameterTypeEnum; -use Smart\SonataBundle\Logger\HistoryLogger; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use Sonata\AdminBundle\FieldDescription\FieldDescriptionInterface; @@ -29,19 +26,16 @@ class ParameterAdmin extends AbstractAdmin { private ParameterTypeEnum $typeEnum; - private HistoryLogger $historyLogger; - private array $updateInitialData = []; + public ?string $showHistoryTemplate = '@SmartSonata/admin/parameter_admin/show_history_field.html.twig'; public function __construct( string $code, ?string $class, string $baseControllerName, ParameterTypeEnum $typeEnum, - HistoryLogger $historyLogger, ) { parent::__construct($code, $class, $baseControllerName); $this->typeEnum = $typeEnum; - $this->historyLogger = $historyLogger; } protected function configureRoutes(RouteCollectionInterface $collection): void @@ -65,7 +59,7 @@ protected function configureListFields(ListMapper $list): void ->add('help', null, ['label' => 'field.label_help']) ->add('value', null, [ 'label' => 'field.label_value', - 'template' => 'admin/parameter_admin/list_value.html.twig' + 'template' => '@SmartSonata/admin/parameter_admin/list_value.html.twig' ]) ; } @@ -118,7 +112,7 @@ protected function configureShowFields(ShowMapper $show): void ->add('value', $valueType, ['label' => 'field.label_value']) ->end() ->with('fieldset.label_history', ['class' => 'col-md-12', 'label' => 'fieldset.label_history']) - ->add('history', null, ['template' => 'admin/parameter_admin/timeline_history_field.html.twig']) + ->add('historyLegacy', null, ['template' => '@SmartSonata/admin/parameter_admin/timeline_history_field.html.twig']) ->end() ; } @@ -197,21 +191,4 @@ protected function configureExportFields(): array $this->trans('field.label_help') => 'help', ]; } - - protected function preUpdate(object $object): void - { - /** @var UnitOfWork $uow @phpstan-ignore-next-line */ - $uow = $this->getModelManager()->getEntityManager($this->getClass())->getUnitOfWork(); - $this->updateInitialData = [ - 'value' => $uow->getOriginalEntityData($object)['value'], - ]; - } - - protected function postUpdate(object $object): void - { - /** @var HistorizableInterface $object */ - $this->historyLogger->logDiff($object, $this->updateInitialData, [ - 'author' => $this->getUser()->getFullName(), // @phpstan-ignore-line - ]); - } } diff --git a/src/Controller/AbstractSecurityController.php b/src/Controller/AbstractSecurityController.php index fe6c61b..c837ab6 100644 --- a/src/Controller/AbstractSecurityController.php +++ b/src/Controller/AbstractSecurityController.php @@ -3,6 +3,7 @@ namespace Smart\SonataBundle\Controller; use Doctrine\ORM\EntityManagerInterface; +use Smart\CoreBundle\EventListener\HistoryDoctrineListener; use Smart\SonataBundle\Form\Type\Security\ForgotPasswordType; use Smart\SonataBundle\Mailer\BaseMailer; use Smart\SonataBundle\Security\Form\Type\ResetPasswordType; @@ -128,7 +129,7 @@ public function forgotPassword(Request $request, ParameterBagInterface $paramete * * @return Response */ - public function resetPassword(Request $request) + public function resetPassword(Request $request, HistoryDoctrineListener $historyListener) { if ($this->getUser()) { return $this->redirectToRoute($this->context . '_dashboard'); @@ -172,6 +173,7 @@ public function resetPassword(Request $request) try { if (null !== $user->getPlainPassword()) { + $historyListener->disable(); $this->updateUser($user); $this->tokenManager->consume($token); } diff --git a/src/Controller/CRUD/SendAccountCreationEmailTrait.php b/src/Controller/CRUD/SendAccountCreationEmailTrait.php index 6a074cd..308127a 100644 --- a/src/Controller/CRUD/SendAccountCreationEmailTrait.php +++ b/src/Controller/CRUD/SendAccountCreationEmailTrait.php @@ -30,7 +30,7 @@ public function sendAccountCreationEmailAction(TokenManagerInterface $tokenManag 'security_reset_password_route' => $context . '_security_reset_password', 'token' => $token->getValue(), ]); - $mailer->send($email, $subject->getEmail()); + $mailer->send($email, $subject); $this->addFlash('success', $translator->trans('send_account_creation_email.success', [ '{email}' => $subject->getEmail() diff --git a/src/Entity/Log/BatchLog.php b/src/Entity/Log/BatchLog.php index 520b037..59ab137 100644 --- a/src/Entity/Log/BatchLog.php +++ b/src/Entity/Log/BatchLog.php @@ -7,6 +7,8 @@ use Doctrine\ORM\Mapping as ORM; /** + * @deprecated use Smart\CoreBundle\Entity\ProcessTrait and Smart\CoreBundle\Entity\ProcessInterface instead + * * @author Louis Fortunier * * @ORM\Table(name="batch_log") diff --git a/src/Entity/Log/HistorizableInterface.php b/src/Entity/Log/HistorizableInterface.php index b0c69be..555b895 100644 --- a/src/Entity/Log/HistorizableInterface.php +++ b/src/Entity/Log/HistorizableInterface.php @@ -3,6 +3,8 @@ namespace Smart\SonataBundle\Entity\Log; /** + * @deprecated use Smart\CoreBundle\Entity\Log\HistorizableInterface instead + * * @author Mathieu Ducrot */ interface HistorizableInterface diff --git a/src/Entity/Log/HistorizableTrait.php b/src/Entity/Log/HistorizableTrait.php index 8a359c3..9e5ee88 100644 --- a/src/Entity/Log/HistorizableTrait.php +++ b/src/Entity/Log/HistorizableTrait.php @@ -7,6 +7,8 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** + * @deprecated use Smart\CoreBundle\Entity\Log\HistorizableTrait instead + * * @author Mathieu Ducrot */ trait HistorizableTrait diff --git a/src/Entity/Parameter.php b/src/Entity/Parameter.php index f14546b..5637365 100644 --- a/src/Entity/Parameter.php +++ b/src/Entity/Parameter.php @@ -4,8 +4,8 @@ use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Smart\CoreBundle\Entity\Log\HistorizableTrait; use Smart\CoreBundle\Utils\ArrayUtils; -use Smart\SonataBundle\Entity\Log\HistorizableTrait; use Smart\SonataBundle\Enum\ParameterTypeEnum; use Sonata\Exporter\Exception\InvalidMethodCallException; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -66,6 +66,12 @@ class Parameter implements ParameterInterface #[Assert\Length(max: 100)] private ?string $regex = null; + /** + * @ORM\Column(name="history_legacy", nullable=true) + */ + #[ORM\Column(name: 'history_legacy', nullable: true)] + protected ?array $historyLegacy = null; + /** * @param mixed $payload * @Assert\Callback @@ -251,4 +257,9 @@ public function setRegex(?string $regex): void { $this->regex = $regex; } + + public function getHistoryLegacy(): ?array + { + return $this->historyLegacy; + } } diff --git a/src/Entity/ParameterInterface.php b/src/Entity/ParameterInterface.php index 14fdce8..568073c 100644 --- a/src/Entity/ParameterInterface.php +++ b/src/Entity/ParameterInterface.php @@ -2,7 +2,7 @@ namespace Smart\SonataBundle\Entity; -use Smart\SonataBundle\Entity\Log\HistorizableInterface; +use Smart\CoreBundle\Entity\Log\HistorizableInterface; /** * @author Mathieu Ducrot diff --git a/src/Entity/User/UserTrait.php b/src/Entity/User/UserTrait.php index acde38f..d61b423 100644 --- a/src/Entity/User/UserTrait.php +++ b/src/Entity/User/UserTrait.php @@ -82,7 +82,7 @@ public function getListDisplay() /** * @return int */ - public function getId() + public function getId(): ?int { return $this->id; } diff --git a/src/Logger/BatchLogger.php b/src/Logger/BatchLogger.php index 9d0c2a1..616da68 100644 --- a/src/Logger/BatchLogger.php +++ b/src/Logger/BatchLogger.php @@ -6,6 +6,11 @@ use Sentry\ClientInterface; use Smart\SonataBundle\Entity\Log\BatchLog; +/** + * @deprecated use Smart\CoreBundle\Monitor\ProcessMonitor instead + * + * @author Louis Fortunier + */ class BatchLogger { private EntityManagerInterface $entityManager; diff --git a/src/Logger/HistoryLogger.php b/src/Logger/HistoryLogger.php index 513bdbc..274ed6f 100644 --- a/src/Logger/HistoryLogger.php +++ b/src/Logger/HistoryLogger.php @@ -6,6 +6,8 @@ use Smart\SonataBundle\Entity\Log\HistorizableInterface; /** + * @deprecated use the Smart\CoreBundle\Logger\HistoryLogger instead + * * @author Mathieu Ducrot */ class HistoryLogger diff --git a/src/Mailer/BaseMailer.php b/src/Mailer/BaseMailer.php index e802b8f..68541d0 100644 --- a/src/Mailer/BaseMailer.php +++ b/src/Mailer/BaseMailer.php @@ -2,8 +2,9 @@ namespace Smart\SonataBundle\Mailer; -use Smart\SonataBundle\Entity\Log\HistorizableInterface; -use Smart\SonataBundle\Logger\HistoryLogger; +use Smart\CoreBundle\Entity\Log\HistorizableInterface; +use Smart\CoreBundle\Entity\MailableInterface; +use Smart\CoreBundle\Logger\HistoryLogger; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mime\Address; @@ -13,17 +14,23 @@ class BaseMailer { private string $senderAddress; private string $senderName = ''; + protected ?string $recipientToString = null; protected MailerInterface $mailer; protected EmailProvider $provider; protected TranslatorInterface $translator; - protected HistoryLogger $logger; + protected HistoryLogger $historyLogger; + protected bool $flushLog = true; - public function __construct(MailerInterface $mailer, EmailProvider $provider, TranslatorInterface $translator, HistoryLogger $logger) - { + public function __construct( + MailerInterface $mailer, + EmailProvider $provider, + TranslatorInterface $translator, + HistoryLogger $historyLogger + ) { $this->mailer = $mailer; $this->provider = $provider; $this->translator = $translator; - $this->logger = $logger; + $this->historyLogger = $historyLogger; } /** @@ -69,10 +76,15 @@ public function send(TemplatedEmail $email, $recipient = null): void $this->mailer->send($email); if ($recipient instanceof HistorizableInterface) { - $this->logger->log($recipient, HistoryLogger::EMAIL_SENT_CODE, [ - 'title' => $email->getSubject(), - 'email_code' => $email->getCode(), - ]); + $historyData = [ + HistoryLogger::TITLE_PROPERTY => $email->getSubject(), + HistoryLogger::RECIPIENT_PROPERTY => $this->recipientToString, + ]; + + $this->historyLogger + ->setFlushLog($this->flushLog) + ->log($recipient, HistoryLogger::EMAIL_SENT_CODE, $historyData) + ; } } @@ -82,12 +94,54 @@ public function send(TemplatedEmail $email, $recipient = null): void */ protected function setRecipientToEmail(TemplatedEmail $email, $recipient = null): void { - if ($recipient == null) { + if ( + ($recipient instanceof MailableInterface && $recipient->getRecipientEmail() === null) + || (is_array($recipient) && empty($recipient)) + || $recipient == null + ) { throw new \InvalidArgumentException($this->translator->trans('smart.email.empty_recipient_error', [ '%code%' => $email->getCode() ], 'email')); } - $email->to($recipient); + if ($recipient instanceof MailableInterface) { + $this->recipientToString = $recipient->getRecipientEmail(); + $email->addTo(...$this->extractEmailFromString($recipient->getRecipientEmail())); + $email->addCc(...$this->extractEmailFromString($recipient->getCc())); + $email->addBcc(...$this->extractEmailFromString($recipient->getCci())); + } elseif (is_array($recipient)) { + $this->recipientToString = implode(', ', $recipient); + $email->addTo(...$recipient); + } else { + $this->recipientToString = $recipient; + $email->to(...$this->extractEmailFromString($recipient)); + } + } + + private function extractEmailFromString(?string $string): array + { + if (null == $string) { + return []; + } + + if (str_contains($string, ',')) { + $separator = ','; + } elseif (str_contains($string, ';')) { + $separator = ';'; + } + if (!empty($separator)) { + $toReturn = array_map(function ($elem) { + return trim($elem); + }, explode($separator, $string)); + } else { + $toReturn = [$string]; + } + + return $toReturn; + } + + public function setFlushLog(bool $flushLog): void + { + $this->flushLog = $flushLog; } } diff --git a/src/Security/EventSubscriber/SecuritySubscriber.php b/src/Security/EventSubscriber/SecuritySubscriber.php index 5d728a2..a3315b8 100644 --- a/src/Security/EventSubscriber/SecuritySubscriber.php +++ b/src/Security/EventSubscriber/SecuritySubscriber.php @@ -2,10 +2,10 @@ namespace Smart\SonataBundle\Security\EventSubscriber; +use Smart\CoreBundle\EventListener\HistoryDoctrineListener; use Smart\SonataBundle\Security\LastLoginInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Component\Security\Http\SecurityEvents; @@ -27,13 +27,16 @@ class SecuritySubscriber implements EventSubscriberInterface */ private $translator; + private HistoryDoctrineListener $historyListener; + /** * @param LastLoginProcessor $lastLoginProcessor */ - public function __construct(LastLoginProcessor $lastLoginProcessor, TranslatorInterface $translator) + public function __construct(LastLoginProcessor $lastLoginProcessor, TranslatorInterface $translator, HistoryDoctrineListener $historyListener) { $this->lastLoginProcessor = $lastLoginProcessor; $this->translator = $translator; + $this->historyListener = $historyListener; } /** @@ -67,6 +70,7 @@ public function onInteractiveLogin(InteractiveLoginEvent $event) return; } + $this->historyListener->disable(); $this->lastLoginProcessor->process($user); } diff --git a/src/Security/SmartUserInterface.php b/src/Security/SmartUserInterface.php index ee8c629..a21cb24 100644 --- a/src/Security/SmartUserInterface.php +++ b/src/Security/SmartUserInterface.php @@ -7,6 +7,8 @@ /** * Add methods for resetting password + * + * todo in v3 extends the MailableInterface and UserProfileInterface */ interface SmartUserInterface extends PasswordAuthenticatedUserInterface, UserInterface { diff --git a/src/Twig/Extension/FormatExtension.php b/src/Twig/Extension/FormatExtension.php new file mode 100644 index 0000000..aad7036 --- /dev/null +++ b/src/Twig/Extension/FormatExtension.php @@ -0,0 +1,32 @@ + + */ +class FormatExtension extends AbstractExtension +{ + public function getFunctions(): array + { + return [ + new TwigFunction('is_iso8601_datetime', [$this, 'isIso8601Datetime']), + ]; + } + + public function isIso8601Datetime(array|string|null $date): bool + { + try { + if ($date == null || is_array($date)) { + return false; + } + $dt = new \DateTime($date); + return $dt->format(\DateTimeInterface::ATOM) === $date; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/src/Twig/Extension/HistoryExtension.php b/src/Twig/Extension/HistoryExtension.php new file mode 100644 index 0000000..24245ee --- /dev/null +++ b/src/Twig/Extension/HistoryExtension.php @@ -0,0 +1,105 @@ + + */ +class HistoryExtension extends AbstractExtension +{ + private TranslatorInterface $translator; + + public function getFunctions(): array + { + return [ + new TwigFunction('history_get_row_icon_name', [$this, 'getRowIconName']), + new TwigFunction('history_get_row_icon_class', [$this, 'getRowIconClass']), + new TwigFunction('history_get_row_icon_prefix', [$this, 'getRowIconPrefix']), + new TwigFunction('history_get_row_title', [$this, 'getRowTitle']), + ]; + } + + public function getRowIconName(array $row): ?string + { + if (isset($row['success'])) { + return 'check'; + } elseif (!isset($row['code'])) { + return null; + } + + return match ($row['code']) { + 'email.sent' => 'envelope', + 'crt' => 'plus', + 'upd' => 'pencil', + 'arc' => 'archive-box', + 'stripe' => 'stripe', + 'api' => 'abbr-api', + 'import' => 'arrow-up-tray', + 'cron' => 'cog', + default => null, + }; + } + + public function getRowIconClass(array $row): string + { + if (isset($row['success'])) { + return 'bg-success'; + } + + return match ($row['code']) { + 'email.sent', 'upd', 'int' => 'bg-info', + 'crt', 'import' => 'bg-success', + 'arc' => 'bg-warning', + 'ext' => 'bg-neutral', + 'err' => 'bg-danger', + 'stripe' => 'bg-indigo-500', + default => 'bg-neutral-light', + }; + } + + public function getRowIconPrefix(array $row): ?string + { + if (!isset($row['code'])) { + return 'heroicons'; + } + + return match ($row['code']) { + 'stripe' => 'bxl', + 'api' => 'gravity-ui', + default => 'heroicons', + }; + } + + public function getRowTitle(array $row, string $domain = 'messages'): ?string + { + $codeTitle = $this->translator->trans('history.' . ($row['code'] ?? null), [], $domain); + $toReturn = $row['title'] ?? null; + + if (!isset($row['code'])) { + return $toReturn; + } + + switch ($row['code']) { + case 'email.sent': + case 'import': + $toReturn = $codeTitle . ($toReturn !== null ? (' : ' . $toReturn) : ''); + break; + case 'crt': + case 'upd': + case 'arc': + $toReturn = $codeTitle; + break; + } + + return $toReturn; + } + + public function setTranslator(TranslatorInterface $translator): void + { + $this->translator = $translator; + } +} diff --git a/templates/admin/base_field/api_call_status_code.html.twig b/templates/admin/base_field/api_call_status_code.html.twig index 401c0ec..523cc49 100644 --- a/templates/admin/base_field/api_call_status_code.html.twig +++ b/templates/admin/base_field/api_call_status_code.html.twig @@ -12,15 +12,19 @@
- {{ value }} + {% if value is null %} + {{ 'enum.process_status.ongoing'|trans({}, 'messages') }} + {% else %} + {{ value }} + {% endif %} - {% if object.restartedAt is not null %} + {% if object is defined and object.restartedAt is defined and object.restartedAt is not null %} {% endif %}
diff --git a/templates/admin/base_field/show_history_field.html.twig b/templates/admin/base_field/show_history_field.html.twig new file mode 100644 index 0000000..c814321 --- /dev/null +++ b/templates/admin/base_field/show_history_field.html.twig @@ -0,0 +1,191 @@ +{% trans_default_domain 'admin' %} +{% from '@SmartSonata/macros/badge.html.twig' import badge as badge %} + +{% macro render_diff_label(field) %} + {% set diff_label_key = ('label.' ~ field|u.snake) %} + {% set translated_diff_label = diff_label_key|trans %} + {% if diff_label_key == translated_diff_label %} + {{ ('field.label_' ~ field|u.snake)|trans }} + {% else %} + {{ translated_diff_label }} + {% endif %} +{% endmacro %} + +{% macro render_diff_value(value) %} + {% if is_iso8601_datetime(value) and value|date('H:i') == '00:00' %} + {{ value|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATE_FR')) }} + {% elseif is_iso8601_datetime(value) and value|date('H:i') != '00:00' %} + {{ value|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATETIME_FR')) }} + {% elseif value is null %} + ({{ 'label.empty'|trans }}) + {% elseif value is same as(true) %} + {{ 'label_type_yes'|trans({}, 'SonataAdminBundle') }} + {% elseif value is same as(false) %} + {{ 'label_type_no'|trans({}, 'SonataAdminBundle') }} + {% elseif value is iterable %} + {% for item in value %} + {{ item }} + {% endfor %} + {% else %} + {{ value|raw|nl2br }} + {% endif %} +{% endmacro %} + +
+ {% if value is empty %} +
+ Aucun historique +
+ {% else %} +
+ {% for row in value %} +
+ {% if not loop.last %} +
+
+
+ {% endif %} + {% set icon_name = history_get_row_icon_name(row) %} +
+ {% block icon %} + {% if icon_name == null %} +
+
+
+ {% else %} +
+ +
+ {% endif %} + {% endblock %} +
+
+
+ +
+
+ {{ history_get_row_title(row) }} + {% if row.ctxt is defined %} + + [{{ ('history.context.' ~ row.ctxt)|trans({}, 'messages') }}{% if row.orgn is defined %} : {{ row.orgn|trans({}, 'messages') }}{% endif %}] + + {% endif %} +
+ {% if row.user is defined %} +
+ {{ 'label.by'|trans }} {{ row.user }} + {% if row.user_prf is defined %}({{ 'label.profile'|trans }} {{ ('label.' ~ row.user_prf)|trans }}){% endif %} +
+ {% endif %} +
+ +
+
+ {% if row.status_code is defined and row.status_code is not null %} +
+ {% include '@SmartSonata/admin/base_field/api_call_status_code.html.twig' with {value: row.status_code} %} +
+ {% endif %} + {{ row.date|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATETIME_FR')) }} +
+ {% block status_update %} + {% if row.status is defined and row.status is not null %} +
+ {% if row.status.f is not null %} + {{ badge((object.statusPrefixLabel ~ row.status.f)|trans({}, 'messages'), row.status.f) }} + {% endif %} + {% if row.status.f is not null and row.status.t is not null %} + + {% endif %} + {% if row.status.t is not null %} + {{ badge((object.statusPrefixLabel ~ row.status.t)|trans({}, 'messages'), row.status.t) }} + {% endif %} +
+ {% endif %} + {% endblock %} +
+
+ + {# Monitoring spécifique #} + {% if row.api_id is defined %} + + API #{{ row.api_id }} + + {% endif %} + {% if row.cron_id is defined %} + + Cron #{{ row.cron_id }} + + {% endif %} + + + {% if row.recipient is defined %} +
Destinataire de l'email : {{ row.recipient }}
+ {% endif %} + {% if row.email_last_status is defined %} +
+
+ {{ 'label.email_status_last'|trans }} :  + {{ ('enum.email_status.' ~ row.email_last_status)|trans({}, 'messages') }} + le {{ row.email_last_status_at|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATETIME_FR')) }} +
+ {% if row.email_status_history is defined and row.email_status_history|length > 1 %} +
+ {{ 'label.email_status_history'|trans }} : +
    + {% for status_history in row.email_status_history %} +
  • + {{ ('enum.email_status.' ~ status_history.status)|trans({}, 'messages') }} + le {{ status_history.status_at|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATETIME_FR')) }} +
  • + {% endfor %} +
+
+ {% endif %} +
+ {% endif %} + + + {% if row.desc is defined %} +
{{ row.desc|raw|nl2br }}
+ {% endif %} + {% if row.comment is defined %} +
{{ row.comment|raw|nl2br }}
+ {% endif %} + {% block custom_content %}{% endblock %} + + + {% if row.diff is defined %} +
    + {% for field, field_data in row.diff %} +
  • + {{ _self.render_diff_label(field) }} : + {% if field_data.c_u is defined and field_data.c_u is not empty %} +
      + {% for item in field_data.c_u %} +
    • {{ item }}
    • + {% endfor %} +
    + {% elseif field_data.f is not null or field_data.t is not null%} + + {% block render_diff_from_value %} + {{ _self.render_diff_value(field_data.f) }} + {% endblock %} + + + {% block render_diff_to_value %} + {{ _self.render_diff_value(field_data.t) }} + {% endblock %} + {% else %} + ({{ field_data|trans }}) + {% endif %} +
  • + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} +
+ {% endif %} +
diff --git a/templates/admin/base_field/timeline_history_field.html.twig b/templates/admin/base_field/timeline_history_field.html.twig index 4671b91..5b263df 100644 --- a/templates/admin/base_field/timeline_history_field.html.twig +++ b/templates/admin/base_field/timeline_history_field.html.twig @@ -1,3 +1,4 @@ +{# @deprecated use show_history_field.html.twig instead #} {% trans_default_domain 'admin' %} {% if value is null or value is empty %} @@ -47,7 +48,7 @@ {% endif %}
- {{ row.date|date('d/m/Y à H:i:s') }} + {{ row.date|date(constant('Smart\\CoreBundle\\Formatter\\PhpFormatter::DATETIME_WITH_SECONDS_FR')) }}

{% if row.data is defined %}